请移步最新随笔:从一道面试题看闭包,更细致,更清晰
https://www.cnblogs.com/yanchenyu/p/10038058.html
==============
说实话,前面一节的原型和原型链在当初学的时候并没有很头疼,对着高级编程第三版撸了几遍就理解透了,闭包这一节真的挺头疼的,很惭愧,看了差不多十来遍吧,还翻看了网上的其他博客和解释文档,五花八门的表达方式,虽然核心思想都一致,但是实在是不能做到自己的理解,后来结合函数作用域链,好不容易有点开窍,趁着热乎劲儿,赶紧写下来,感兴趣的可以参考一下。
闭包:
高级编程上面的解释是指有权访问另一个函数作用域中的变量的函数,(是一个函数);
创建闭包的常见方式,就是在一个函数内部创建另一个函数。
在理解闭包之前,先要清楚一个函数在创建到调用再到结束后的一系列过程:
当一个函数被调用的时候,会先创建一个执行环境以及相应的作用域链,然后用arguments对象和其他命名参数的值来初始化函数的活动对象,但在作用域链中,外部函数的活动对象始终是排在第二,外部函数的外部函数的活动对象排在第三。。。。。。最后一个是全局执行环境下的活动对象。(一圈一圈往外,就像射箭的靶子,中间的红心就是当前的执行环境下的活动对象)
以一个函数为例:
function compare(value1, value2){ return …… } var result = compare(5, 10);
在创建函数compare()的时候,会先创建一个预先包含全局变量对象的作用域链,这个作用域链被保存在内部的[[Scope]](就是作用域)属性中,当调用compare()函数的时候,会为函数创建一个执行环境,然后通过复制[[Scope]]中的对象来构建起执行环境的作用域链,此后会将活动对象推到执行环境作用域链的前端。此时的compare()的作用域链上有两个变量对象,第一个是本地变量对象:arguments,value1,value2;第二个则是全局变量对象:result,compare。当函数执行完毕后,局部活动对象就会被销毁,只留下全局执行环境下的变量对象。
但是闭包的情况又有所不同:
如果在函数内部再定义一个函数,则里面的函数会将外部函数的活动对象添加到它的作用域链中,但是它的作用域链的前端还是它自己的活动对象,后面依次是外部函数的活动对象,外部函数的外部函数的活动对象。。。。。直到全局执行环境下的变量对象。
function compare(value1, value2){ return function(name){ ……. } } var compareA = compare(1,2); var result = compareA(“aaaa”);
当匿名函数在compare()中被返回之后,它的作用域链会被初始化,包含compare()的活动对象以及全局变量对象。当compare()执行完毕后,它的活动对象不会被销毁,因为匿名函数的作用域链仍然在引用这个活动对象。也就是说,compare()函数返回后,它的执行环境的作用域链会被销毁,但是它的活动对象仍然保留在内存中,直到匿名函数被销毁后,它的活动对象才会销毁。拿上面的例子来说明,当执行compare(1,2)的时候,如果没有闭包,则执行完后compare的执行环境就会被销毁,活动对象也不会保留在内存中,内存中只剩下全局变量对象,但是如果函数内部有闭包,此时里面的内部函数返回了,这时候初始化内部函数的作用域链,会保留内部函数外的活动对象(也就是compare的活动对象),还有全局变量对象在内存中,所以这时候,compare的活动对象还是可以访问的,没有被销毁,虽然此时的compare的作用域链已经销毁了。
=====
做一个总结:
当一个函数被创建的时候,同时会产生一个作用域链,这个作用域链是全新的,上面没有什么当前活动对象,只有全局对象,这条作用域链是核心链子,保存在内部”作用域“属性中,一直都在的,当函数被调用的时候,会先创造一个执行环境(关于执行环境,活动对象,变量对象这些名词请参考第一篇随笔),然后复制刚刚内部的“作用域”中的对象来构建一条执行环境的作用域链,这条跟创建函数时所保存的作用域链不是同一条,这条是专门给执行环境准备的,所以当前变量对象,也就是活动对象会被顶到这条作用域链的最前端,此时这条链子上有两个变量对象,最前面的是当前活动对象,后面的是全局环境下定义的变量对象,如果没有闭包,当函数执行完毕之后,当前执行环境就会销毁,同时当前活动对象也会销毁,作用域链上面只剩下全局环境下的变量对象。在第一篇中我们说到如何访问一个变量,标识符解析的时候就是沿着作用域链逐层向上找。所以这时候我们要访问当前变量对象就没办法了。
此时闭包的作用就有了。
在函数内部返回一个新函数,此时新函数被调用,那么此时又会给新函数创建一个新的执行环境,同样当前执行环境下的变量对象(也是活动对象)就会被顶到作用域链的最前端,而刚刚那个外层的函数的执行环境下的变量对象就被挤到了第二个,由于外层的函数已经执行完了,这时候它对应的执行环境已经销毁,但是它的执行环境下的变量对象还在这条作用域链上,在第二个,所以我们依然可以通过这条作用域链来访问到刚刚已经执行过的函数的执行环境下所定义的变量对象,虽然它已经执行过并且被销毁了。
我再做进一步总结:
闭包的作用:能够访问到内部函数的变量对象,
实现方法:在函数内部再返回一个新函数
实现原理:在作用域链上,在要访问的变量对象前面再塞一个新的变量对象,就能保证要访问的变量对象不会销毁并且可以访问到了。(就像串糖葫芦一样,你担心最上面那颗可能会掉,那就在那颗上面再串一颗,就能保证第二颗不掉了)
=====
闭包与变量:
闭包只能取得包含函数(外部函数)中的任何变量的最后一个值。闭包保存的是整个变量对象,而不是某个特殊的变量。
下面是经典案例:
function createFunctions(){ var result = new Array(); for(var i = 0; i<10; i++){ result[i] = function(){ return i } } return result }
每个函数都会返回10,因为每个函数中的作用域链中都保存着createFunctions()的活动对象,而它的活动对象是i,当createFunctions()执行完毕后,i的值为10,所以每个函数的作用域中的内部i都是10。这是按引用传递,如果想要达到我们期待的结果,就要按值传递。
function createFunctions(){ var result = new Array(); for(var i = 0; i<10; i++){ result[i] = (function(num){ return function(){ return num } })(i) } return result }
关于this对象:
匿名函数的执行环境具有全局性,因此其this对象通常指向window。所以闭包中的this通常是全局变量对象下的。
=========
2018年3月1日补充:
闭包的场景:
1.使用闭包可以在JavaScript中模拟块级作用域;
2.闭包可以用于在对象中创建私有变量。