JS执行环境、作用域链与闭包

参考文献:

《JavaScript高级程序设计(第三版)》第4章+第7章

《JavaScript面向对象编程指南(第二版)》第3章

有一个比较好的链接,待看:

https://blog.csdn.net/Eternal_tyq/article/details/81914025

本文是一个初步的理解,还有待深入研究。

1.相关概念汇总

1.当创建函数时,会为函数创建一个预先包含全局变量对象的作用域链,这个作用域链保存在内部的[[scope]]属性中。

2.当调用函数时,会为函数创建一个执行环境,然后通过复制函数的[[scope]]属性中的对象构建起执行环境的作用域链

3.此后,又有一个活动对象(在此作为变量对象使用)被创建并被推入执行环境作用域链的前端。可以看出,作用域链本质上是一个指向变量对象的指针列表。

4.作用域链是在相应的执行环境执行完毕回到主干上时就被销毁了。

5.当存在闭包时,闭包所在的包含函数执行完成后,这个执行环境的作用域链被销毁,但是闭包会引用该包含函数的变量对象,因此包含函数的变量对象没有被销毁。只有解除了对闭包函数的引用,闭包对应的包含函数的变量对象才会被销毁。

  • 执行环境(execution context,环境):当函数被调用时,就形成了一个执行环境,此时函数中的变量或者函数将在程序执行到它们的时候被创建。
  • 作用域链:执行环境对应一个作用域链,作用域链的本质是一个指向多个变量对象的指针列表,当执行环境结束时,作用域链被销毁。
  • 变量对象:包含了this、arguments和其他命名参数的值来初始化的对象。变量对象分为全局变量对象和局部变量对象,前者为window的变量对象,后者为函数的局部对象,又称为活动对象(activation object)。

1.1 作用域链

  1. 内部环境可以通过作用域链访问所有的外部环境,但外部环境不能访问内部环境中的任何变量和函数。这些环境之间的联系是线性、有次序的。每个环境都可以向上搜索作用域链,以查询变量和函数名;但任何环境都不能通过向下搜索作用域链而进入另一个执行环境。
  2. 执行环境的类型总共只有两种——全局和局部(函数)
  3. 除了使用添加函数来延长作用域链,还有两种语句能够延长作用域链

    (1)try-catch 语句的catch 块;
    (2)with 语句。

JS执行环境、作用域链与闭包    JS执行环境、作用域链与闭包

对上述例子的分析:

      作用域链是由变量对象的引用所构成的,且每个执行环境都绑定一个作用域链和自身的变量对象,这个例子中window的执行环境是全局执行环境,作用域链只包括自身的变量对象,这里我们主要来看swapColors()这个执行环境的作用域链、changeColor()这个执行环境的作用域链。引用《JavaScript高级程序设计(第三版)》中的分析:

  1. 对于这个例子中的swapColors()而言,其作用域链中包含3 个变量对象:swapColors()的变量对象、changeColor()的变量对象和全局变量对象。swapColors()的局部环境开始时会先在自己的变量对象中搜索变量和函数名,如果搜索不到则再搜索上一级作用域链。changeColor()的作用域链中只包含两个对象:它自己的变量对象和全局变量对象。这也就是说,它不能访问swapColors()的环境。
  2. 函数参数也被当作变量来对待,因此其访问规则与执行环境中的其他变量相同。

练习1

JS执行环境、作用域链与闭包

JS执行环境、作用域链与闭包

练习2

JS执行环境、作用域链与闭包

JS执行环境、作用域链与闭包

 

2.闭包

慎用闭包。

JS执行环境、作用域链与闭包

2.1 普通闭包

(1)闭包的常规用法

访问函数隐藏变量(F中的b)

分析:

  1. 函数F在执行时,创建了函数N,函数N使用了函数F变量对象中的参数b,因此当函数F执行结束后,虽然F相关的作用链被销毁,但是F的变量对象依然存在,被函数N的作用域链引用。

闭包的释放:何时能够释放F的变量对象?似乎不是函数N执行结束,而是在使用完闭包以后将inner置为null。原理还是有些模糊。

JS执行环境、作用域链与闭包                

JS执行环境、作用域链与闭包

JS执行环境、作用域链与闭包

(2)闭包的全局用法

与例2.1-1不同的是,这里不是在F中用return返回闭包函数,而是直接将闭包函数赋值给一个全局变量。二者效果相同。

分析:

  1. 函数F在执行时,按顺序做了如下事情:创建变量b,创建函数N且函数N使用了b,让全局变量inner指向函数N。
  2. 当函数F执行完成后,调用inner()等于调用N(),虽然此时F的执行环境结束了,但是由于函数N对F变量对象的引用,因此F的变量对象依然存在,所以依然可以访问b。

闭包的释放:我觉得应该是让inner=null;这样无法访问闭包了,就可以释放F的变量对象了。

JS执行环境、作用域链与闭包

JS执行环境、作用域链与闭包

JS执行环境、作用域链与闭包

(3)闭包的特性

保存的始终是包含函数变量对象的最新值

分析:

  1. 当调用函数F时,程序进入F的执行环境,分别进行了:创建函数N且引用了F变量对象的param变量,对param变量自增1。
  2. 当返回N时,函数F执行结束,虽然F的作用域链销毁了,但是闭包函数N执行时引用了F变量对象中的param变量,因此F的变量对象没有销毁。

闭包的销毁:将inner=null;

JS执行环境、作用域链与闭包

JS执行环境、作用域链与闭包

(4)闭包的特性

分析:

  1. 当执行f时,创建了函数n且引用了f变量对象中的a变量,将a赋值为2,注意这里有变量提升现象。
  2. 最后是执行函数n,在执行函数n时,函数f的执行环境当然还在的,那么f的变量对象当然也还在,因此alert的就是a的最新值,2。
  3. 这里也说明了对变量的搜索会优先内部函数,其次才是外部函数的。

闭包的销毁:不知道f执行结束后,会不会内存泄露。

JS执行环境、作用域链与闭包

(5)完整例子1

函数定义和调用                                                                                                              

JS执行环境、作用域链与闭包    

JS执行环境、作用域链与闭包

分析:执行环境、作用域链、变量对象

JS执行环境、作用域链与闭包

JS执行环境、作用域链与闭包

(6)完整例子2

JS执行环境、作用域链与闭包

JS执行环境、作用域链与闭包

2.2 循环中的闭包

(1)常规循环中的闭包

分析

  1. 当函数F执行时,创建了数组arr,变量i,且在一个3次的循环中为数组赋值,数组的每个元素都指向一个匿名闭包函数(使用了F变量对象中的i),也就是创建了3个匿名函数。
  2. 当F执行完毕后,F的作用域链销毁,但是F的变量对象还在,因为有闭包。当执行数组中每个元素对应的函数时,返回的结果都是i,这个i此时是F变量对象中最后一个值,为3。也就是说,变量对象中只会保留最新的值

函数定义和调用                                                                                         执行结果                                                                           

JS执行环境、作用域链与闭包                             JS执行环境、作用域链与闭包

JS执行环境、作用域链与闭包

(2)立即执行函数

分析:

  1. 函数F执行时,创建了数组arr,变量i,并且用3次循环为数组中的每个元素赋值,数组中元素为立即执行函数的结果,传入的实参为i。因此在F的执行环境中走到为数组元素赋值的这一步时,又创建了一个新的函数(假设名为i-th-new),并且执行了。
  2. 程序进入了一个新的执行环境,此时i-th-new的变量对象中的x为i的副本,i-th-new函数的返回结果赋值给了数组arr[i],结果也是一个闭包函数,这个闭包使用了i-th-new的变量对象中的参数x。
  3. 当arr[0]()执行时,返回的x就是i-th-new的变量对象中的x,也就是记录了i的副本,也正是由于闭包的存在i-th-new的变量对象没有销毁,所以能够访问成功。

闭包的销毁:F的变量对象什么时候销毁的?

JS执行环境、作用域链与闭包                                     JS执行环境、作用域链与闭包

(3)立即执行函数的改进

分析:

  1. 执行F时,创建了函数binder(x),创建了数组arr,变量i,并且用3次循环为数组元素赋值,赋的值为调用binder(i)的返回结果。
  2. 在调用函数binder(i)时,进入binder函数的执行环境,创建了binder的变量对象,变量对象中的x为实参i的副本,binder函数执行结束后,返回了一个闭包函数,闭包函数中使用了binder函数的参数x。
  3. 当执行arr[i]()时,其实就是执行的function(){return x;},这里的x为其包含函数binder的变量对象的x参数,保存的是实参i的副本。

JS执行环境、作用域链与闭包                             JS执行环境、作用域链与闭包

JS执行环境、作用域链与闭包

(4)高级教程上的例子和分析

JS执行环境、作用域链与闭包

JS执行环境、作用域链与闭包

JS执行环境、作用域链与闭包

JS执行环境、作用域链与闭包

2.3 闭包的应用

(1)getter与setter

分析:

  1. 创建了变量getValue和setValue,创建了一个匿名函数并且立即执行,转入新的执行环境。
  2. 在匿名函数的执行环境中,创建了其变量对象的参数secret并赋值为0。
  3. 接着创建了两个匿名函数并赋值给了两个全局变量。这里两个匿名函数能够访问包含函数的变量对象的参数secret,因此也是闭包函数。
  4. 当立即执行函数执行完毕后,getValue和setValue才指向了相应的函数,接着调用getValue(),也就是返回secret,因为闭包的存在。最外面的匿名函数的变量对象暂时不会被销毁,因此可以返回0。
  5. 接着调用setValue(123),也是因为闭包的存在,因此能够更新最外面匿名函数变量对象中的参数secret,使其变为123。
  6. 当再次调用getValue()时,此时最外面笔名函数变量对象中的参数secret的最新值为123,不再是0了。
  7. ...省略

JS执行环境、作用域链与闭包                           

JS执行环境、作用域链与闭包

JS执行环境、作用域链与闭包

(2)迭代器next()

分析:

  1. 当调用setup(['a','b','c'])时,进入setup的执行环境,创建了i并赋值为0,返回了一个闭包函数(使用了setup变量对象的参数i和x)。
  2. 当执行next()时,进入匿名函数的执行环境,因为闭包的存在,setup变量对象依然存在,所以可以成功返回x[i++],因为此时i为0,因此返回的是'a',并将i更新为1。
  3. 当再次执行next()时,跟上述分析一样,只是此时i为1,x不变,所以返回的为'b',将i更新为2
  4. 当再次执行next()时,跟上述分析一样,只是此时i为2,x不变,所以返回的为'c',将i更新为3

JS执行环境、作用域链与闭包

JS执行环境、作用域链与闭包

JS执行环境、作用域链与闭包

JS执行环境、作用域链与闭包