【共读】《你不知道的JavaScript上》做用域与闭包

本文会用导图梳理本书的脉络,因为是导读,正文部分只会列举重点内容,非重点内容会简单介绍,欢迎讨论与阅读原文。此外本文适合未读过此书的同窗参考是否须要阅读,另外读过此书的同窗,能够尝试回答文初的问题及顺着导图回忆本书内容,若是很是流畅那么相信您对书中的知识的理解是过关的。面试

上一篇咱们讲了本书第一章中的做用域,做用域链,简单介绍了引擎,编译器,做用域是怎么合做进行编译的。本篇咱们将会介绍本书第一部分的 2 ~ 6 章。缓存

问题

  1. 说说你对闭包的理解。(面试)
  2. 说说你对词法做用域的理解,咱们如何欺骗词法做用域?
  3. 函数做用域的做用?如何避免函数做用域污染全局变量?
  4. JavaScript 除了函数做用域还存在哪些块做用域?
  5. 变量提高的机制是什么?函数声明和变量什么那个优先被提高?

第一部分做用域与闭包

说说你对闭包的理解? (我的理解,求拍砖,不太了解闭包的同窗能够暂时跳过)性能优化

闭包是一个绑定了执行环境的函数,闭包和普通函数的区别是携带了执行的环境。bash

闭包由环境和表达式两部分组成。闭包

  • 环境部分
    • 环境:函数的词法做用域(执行上下文的一部分)。
    • 标识符列表:函数中用到的未声明的变量。
  • 表达式部分:函数体。

它被普遍应用,好比在回调函数中定时器,事件监听器,Ajax请求,跨窗口通讯……又好比咱们了解的循环,还有模块。闭包能够赋予了咱们访问与操做上级做用域的能力为开发提供了极大的便利。异步

若是要说闭包的缺点可能就是维护困难,闭包能够缓存上级做用域的变量,若是闭包又是异步执行必定要搞清楚上级做用域都发生了什么,对代码的运行机制和逻辑要有所了解。函数

1、词法做用域?

  1. 做用域分红词法做用域动态做用域post

    词法做用域是一套关于引擎如何寻找变量以及在何处找到变量的规则,词法做用域最重要的特征就是它定义的过程发生在代码的书写阶段(假设没有使用eval() or with)。性能

    动态做用域让做用域在运行时动态肯定。优化

    词法阶段就是你说了一句话,而后编译器断句的过程。像不一样的人理解一句话不一样断句也就不一样同样,不一样的编译器分词也会有所不一样。

  2. 词法做用域中有环境记录器外部环境的引用,想要深刻理解,咱们须要了解执行上下文和执行栈(书上没有提到)

  3. 查找过程

    function foo(a) {
        var b = "Java"
        function bar(c) {
            var b = "你好"
            console.log(a + b + c)
        }
        bar("Script")
    }
    foo("你不知道的")
    复制代码

    “遮蔽效应” 做用域查找会在找到第一个匹配的标识符时中止,咱们在多层嵌套的做用域中能够定义同名的标识符,就可能由于遮蔽效应而找不到外部的标识符。

    “全局对象” 全局变量会自动成为全局对象的属性,所以能够不直接经过全局对象的词法名称查找。

    window.a  
    复制代码

    经过这种方式咱们能够访问被遮蔽的全局变量,可是非全局的变量若是被遮蔽了,不管如何也访问不了。

  4. 改变词法做用域的方法?

    词法做用域彻底由写代码期间函数声明的位置来定义,怎么能够在运行时修改呢?

    改变词法做用域的方法有两种,evalwith

    JavaScript 引擎会在编译阶段进行数项的性能优化,其中有些优化依赖于词法的静态分析,必须预先肯定全部变量和函数的定义位置,一但在引擎中发现了上述的两种方法,那么这些优化都是无效的。

    因此这两种方式不建议使用,想看的同窗能够查资料或者翻阅本书。

2、 函数做用域与块做用域

  1. 函数做用域是什么?

    每声明一个函数,就会为自身建立一个做用域,在这个做用域中,属于函数的所有变量均可以在整个函数的范围内(包括嵌套在内的做用域)使用及复用。

  2. 函数做用域的做用?

    隐藏,在当前函数做用域内声明的变量或函数都会绑定在当前的函数做用域中,这样在全局做用域中就访问不到它们了,咱们能够叫这个函数叫包装函数

    这符合软件设计中应该最小限度得暴露必要内容,将其余内容都“隐藏”起来的原则,好比模块或对象API设计。

    规避冲突,避免同名标识符之间的冲突。

  3. 当即执行函数表达式的做用?

    (function foo() {
        var a = "zhengyang";
        console.log(a);
    })();
    复制代码

    因为函数被包含在一对()中就成为了一个表达式,经过在末尾加一个()能够当即执行这个函数。它能够避免 foo 函数名称污染全局做用域,而且也不须要经过函数名 foo()来调用。

    社区称它为LIFE,而且咱们每每使用匿名函数,即省略掉 foo,由于函数表达是能够匿名的。

  4. let的机制?

    let关键字能够将变量绑定到所在的任意做用域中(一般是在{}中),能够说let为其声明的变量隐式得劫持了所在的块做用域。(表面上 JavaScript不具备块做用域的相关功能,但其实 with ,try/catch 是能够建立块做用域的)

3、 提高

  1. 包括变量和函数在内的全部声明都会在任何代码被执行前首先被处理。

  2. 函数声明优先于变量声明。

    咱们把 var a = 2 看作一个声明,事实上这个表达式能够拆成 var a;a = 2 ,第一部分是变量声明发生在编译阶段,第二部分的赋值会被留在原地等待执行阶段。

    console.log(zy) // ƒ zy() {}
    
    function zy() {
    }
    console.log(zy); // ƒ zy() {}
    var zy = 2
    
    console.log(zy); // 2
    复制代码

    在上面的代码中在编译阶段会进行声明的提高,首先提高函数声明 function zy(){} 而后再提高 var zy ,而后逐条执行,因此一个 console.log(zy) 输出了函数 , 而后 第二个console.log(zy) 输出了函数,最后咱们用留在原地的 zy = 2 把 2 赋给了 zy 因此最后一个 console.log(zy) 输出了 2。

4、 循环与闭包

for (var i = 0; i < 5; i++) {
  setTimeout(() => {
    console.log(i)
  }, i * 1000);
}
复制代码

不难理解以上的代码输出是 5 个 5,由于 setTimeout 是异步的因此会在for循环结束后才会执行。可是咱们想要获得的倒是每循环一次输出一次,结果是 1 , 2 , 3 , 4 , 5。

使用闭包的方法解决问题。

for (var i = 1; i <= 5; i++) {
  (function () {
    var j = i;
    setTimeout(() =>{
      console.log(j)
    }, j * 1000)
  })();
}
复制代码

在上面的代码中咱们的环境是全局做用域,函数体是一个当即执行函数,每次的循环 i 的值都由于闭包被保留了下来传递给 j 而后经过 setTimeout 输出一次值。

for (var i = 1; i <= 5; i++) {
 (function (j) {
   setTimeout(() =>{
     console.log(j)
   }, j * 1000)
 })(i);
}
复制代码

咱们也能够用 let

for (let i = 1; i <= 5; i++){
    setTimeout(() =>{
        console.log(i)
    }, i*1000)
}
复制代码

后言

闭包这块我读到这里已然对它有了一个全新的认识,可是仍然存在很多问题,好比具体编译的过程;JavaScript引擎如何作优化;同步异步的理解还要有待挖掘,对于 let 的实现也不是很了解。

相关文章
相关标签/搜索