乐趣区

结合作用域执行上下文图解闭包

一 作用域相关
      作用域是一套规则,用来管理引擎如何查找变量。在 es5 之前,js 只有全局作用域及函数作用域。es6 引入了块级作用域。但是这个块级别作用域需要注意的是不是{} 的作用域,而是 let,const 关键字的块作用域。

1 作用域
1.1 全局作用域
      在全局环境下定义的变量,是挂载在 window 下的。如下代码所示:

1.2 函数作用域

      在函数内定义的变量,值在函数内部才生效,在函数外引用会报 RefrenceError 的错误

      注意区分 RefrenceError 及 TypeError。RefrenceError 是在作用域内找不到,而 TypeError 则是类型错误。如果只是定义了变量 a 直接调用便会报 TypeError 的错误。

1.3 块作用域

      es 新增的关键字 let,const 是作用在块级作用域。但是在 js 内 {} 形成的块,是不具有作用域的概念的。如下所示,虽然 for 循环有一个 {} 包裹的块,但是在块外面还是可以访问 i 的。

2 作用域链

      所谓作用域链,是由当前环境与上层环境的一系列变量对象组成,它保证当前执行环境对符合访问权限的变量和函数的有序访问。而作用域的最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。

      如上图所示,会形成一个 inner 作用域到 outer 作用域到全局作用域的作用域链。当我们在执行 inner 函数的时候,需要 outName 的变量,在自己的作用域内找不到,便会顺着作用域链往上找,直到找到全局作用域。在这个例子中,往上查找到 outer 作用域的时候便找到了。

      简单测试 1: 如下图所示的代码,大家觉得会输出什么呢?

      虽然 fn 的调用是在 show 内调用的,但是因为 fn 所在的作用域是全局作用域,它的 x 的值会顺着作用域链去全局作用域中啊,即 x 会输出 10。这里需要注意的一点是,变量的确定是在函数定义时候确定的,而不是函数运行时。

二 执行上下文相关

      函数每次被调用时,都会产生一个新的执行上下文环境。全局上下文是存在栈中的。而处于栈顶的全局上下文一旦执行完就会自动出栈。如下图所示的代码。

      首先是全局上下文入栈,然后开始执行可执行代码。遇到 outer(),激活 outer()的上下文;

      第二步,outer 的上下文入栈。开始执行 outer 内的可执行代码,直到遇到 inner()。激活 inner()的上下文;

      第三步,inner 的上下文入栈。开始执行 inner 内的可执行代码。执行完毕之后 inner 出栈。

      第四步,inner 的上下文出栈。outer 内继续执行可执行代码。如果一直没有其他的执行上下文,执行完毕即可出栈;

      第五步,outer 的上下文出栈。

      ps: 全局上下文只有浏览器关闭的时候才会出栈。

      那我们已经直到了全局上下文的宏观入栈出栈的概念。具体的全局上下文包括哪些内容,具体做了什么操作呢?

      其实,执行上下文分为准备阶段和执行阶段。

      1. 在执行上下文的准备阶段,会有以下步骤:

            1.1 创建变量对象:初始化 arguments,函数声明提升,变量声明提升等

            1.3 建立作用域链

     2. 而在执行上下文的执行阶段,会有以下步骤:

            2.1 变量赋值

            2.2 函数引用

            2.3 确定 this 指向

            2.4 执行代码

      而在变量对象的创建过程,会经历以下的步骤。

            1. 创建 arguments 对象。也就是当前上下文中的参数;

            2. 检查当前上下文的函数声明,即用 function 关键字声明的函数;

            3. 检查当前上下文的变量声明,即变量,属性值为 undefined。

      而这个创建过程最重要的概念就是提升:

      而如下图所示的代码执行,变量对象的变化过程是怎样的呢?

      那函数内的三个 console 分别会输出什么呢?
      因为在变量对象的创建过程中,是 arguments=> 函数声明 => 变量声明的过程。在第一个 console 之前 function foo()已经被提升,因此第一次输出的该函数,而第二个 console 之前 bar 被提升,并赋值为 undefined,因此第二次输出的是 undefined。而第三个 console 之前 foo 被重新赋值,因此第三个 console 是 ’hello’。

      总结起来,变量对象和活动对象其实是同一个对象,他们只是在执行上下文的不同阶段的状态而已。

      下面的截图即是两个阶段的变化。其实变量对象和活动对象是同一个对象,他们只是执行上下文在不同阶段的不同表现形式。在执行阶段变量对象 V0 会变成活动对象 A0。内部的一些引用也会发生变化。

      而如下图所示的代码执行,分别会输出什么呢?

      首先,第一段代码。函数声明首先会被提升第一个 console 输出 hello world。但是后面的 hello 会被覆盖,第二个 console 输出 hello

      第二段代码。函数声明首先会被提升,但是紧接着会被变量赋值覆盖。因此,两个 console 输出 hello。
总结起来,全局上下文的整个过程即下图所示

      那结合作用域即全局上下文呢,我们一开始的代码代码具体的图解就是下面这张图了。

三 闭包相关
1 闭包分析

      此时,当我们修改 inner 函数,返回上级作用域的 outerName 属性时,闭包就产生了。

      这里为什么会产生闭包呢?具体可以参考下方的图示。
      前面的全局入栈和 outer 函数入栈还是跟原来一样,但是当我们的 outer 函数入栈执行完毕准备出栈,准备被回收的时候,由于 outName 还被 inner 的作用域引用,不能被回收,产生了闭包。

      即所谓的闭包就是通过函数调用,外部持有函数的句柄,让函数的空间不能消失。产生的这块独体的空间永远存在,这块内存对外也是封闭的。所以就叫闭包。

2 常见问题分析

      相信大家在面试的时候会经常问到这样的面试题。下面这段代码输入的是什么呢?

      这里输出的是 5 个 6。需要解释这个问题呢,要涉及到 js 的的执行环境及作用域链了。

      js 的执行环境:JS 是单线程环境,即代码的执行是从上到下,依次执行。这样的执行称为同步执行。因为种种不要浪费和节约的原因。JS 中引进了异步的机制。这块具体的执行逻辑可以参考 https://segmentfault.com/a/11…。在这里,for 循环是同步代码,会先从上到下执行。而 setTimeout 中的是异步代码会将其插入到任务队列当中等待。因此在 setTimeout 执行的时候,for 循环已经执行完成,i 已经变成 6。作用域链。当 setTimeout 执行的时候,会向上去查找 i 的值。往上查找,即 for 所在的作用域,已经是 6 了。因此 6 次 setTimeout 都会输出 6。

      那可能面试官会继续问,我们怎样才能依次输出 1 - 5 呢?这里就可以用到闭包来解决了。

      我们将 i 作为参数传递,并且形成了一个新的立即执行函数作用域。当 setTimeout 执行的时候,去查找 i。即在立即执行函数作用域查找,此时的 i 我们可以根据上面一部分的分析,形成了闭包之后,它的内存是不会消失的。因此这每次循环的时候都是当前 i 即 1 -5。

3 闭包的查看

      其实,我们在 chrome 的控制台是可以去查看闭包的。在浏览器断点调试,可以去观察下面两幅图的红色圈区别。第二副图可以看到 closure,i 值是 1。依次执行,可以看到 i 从 1 到 5 的变化。

 

退出移动版