JS高级程序设计(3rd)中对闭包的定义就是一句话,首先闭包是一个函数,怎样的函数呢?有权访问另一个函数作用域中的变量 的函数。而创建闭包的常见方式就是在一个函数的内部创建另一个函数,就是嵌套函数。
闭包会涉及到的点主要有
① 作用域链(这个原理让我们明白内部嵌套的函数是能够访问外部父函数里定义的变量的,而对于嵌套函数来说引用了非自己作用域内定义的这些变量通常又被称为自由变量)
② 函数执行的机制(这个又涉及到执行环境execution context ,环境栈(注意环境栈的底部永远是全局上下文global context),活动对象,变量对象等)。
先说执行环境相关的:
当函数执行时,函数的环境会被推入到环境栈的顶部,理论上函数执行完毕后会将其环境弹出pop,将控制权返回给之前的执行环境,但是由于有了闭包,情况又会有所不同。
再来说两个相关概念:活动对象(activation object)和变量对象(variable object)
变量对象:每个执行环境都会有自己的变量对象,里面存储着在该执行环境中定义的变量和函数;
如果这个执行环境是函数Function Context,那么则将其活动对象作为变量对象,活动对象中还包含了arguments(形参类数组)、formal parameters(形参的值),活动对象还包含了Argument 对象,该对象具有callee,length等属性
理解了以上两点,现在可以来说一下函数执行的整体流程了:
1、执行代码,全局执行环境
创建global . variable object
2、全局变量的赋值 or 调用函数
调用函数时,会得到当前函数的活动对象activation object,该活动对象中包含了该函数内部变量的声明,函数的声明,形参
3、进入所调用的函数的上下文
进行该函数所在作用域上的变量的赋值及各种运算(此时的作用域包括全局的variable object和当前函数执行环境的activation object)
4、分为三种情况
a、函数正常return或结束,该函数执行环境被弹出,回到step2继续执行其他代码;
b、若在函数中有内部函数的调用,执行step 3;
c、若函数返回了另一个函数,且该函数有对自由变量的引用,则形成闭包。此时作用域链机制仍然有效,当前的执行环境Function Context不会被弹出环境栈,函数的活动对象也留在了内存中,不会在调用结束后被垃圾回收机制回收。回到step2继续执行其他代码;
5、所有代码执行完毕,程序关闭,释放内存
③ 垃圾回收机制
一般来说,一个函数在执行开始的时候,会给其中定义的变量划分内存空间保存,以便后面的语句所用,等到函数执行完毕返回了,这些变量就被认为是无用的了,对应的内存空间也就被回收了。下次再执行此函数的时候,所有变量又回到了最初状态,重新赋值使用。
但是如果一个函数parent内部又嵌套了另一个函数child,而这个child函数又是可能在外部被调用到的,并且这个内部嵌套函数child又使用了外部函数parent中的某些变量,这个时候就形成了闭包,此时的内存回收机制就会与前面一般情况有所不同。
在外部函数parent执行返回后 又直接调用了内部嵌套函数,如果按一般回收情况这时候parent已经执行完毕被回收了,那么内部嵌套函数就没法读取已经被回收的变量,所以在遇到闭包时,JS解析器实际上会将内部嵌套函数本身和父级和祖先级的变量(自由变量)一起保存起来,保存在该闭包中。并且这些变量不会被内存回收器回收。只有当这些内部函数不可能被调用之后(比如被删除了或者没有了指针)才会销毁这个闭包,同时那些不再被该闭包引用的变量才会在下一次内存回收启动时被回收。
由于闭包会使得函数中的变量一直都被保存在内存中,使得内存消耗很大,影响网页性能。所以我们有必要在函数执行完毕后对其进行手动销毁。
下面我们来用例子说明闭包的一些特性,闭包常见的有两种形式--函数作为返回值,函数作为参数传递
看个例子:
1 function test() { 2 var num = 1; 3 return function() { 4 num++; 5 console.log(num) 6 } 7 } 8 var anotherTest = test(); 9 anotherTest();//2 10 anotherTest();//3
以上可以看出闭包让其引用的变量一直存在在内存中(注意虽然活动对象没有被销毁,其包含的变量函数依然存在在内存中,但是却不能直接调用哦)
再来个例子:
1 var result = []; 2 function test() { 3 var num = 0; 4 for (; num < 3; num++) { 5 result[num] = function() { 6 console.log(num); 7 } 8 } 9 } 10 test(); 11 result[0](); // 3 12 result[1](); // 3 13 result[2](); // 3
上面这段代码中,本意是想让test中的变量 i 被内部匿名函数循环使用并依次输出索引0 1 2,但结果却与预想不同。为什么呢?因为闭包中记录的自由变量只是对自由变量的一个引用,也就说只能取得该变量最后状态保留的那个值。本例中执行完for循环后 i 变量最后的值是3,所以所有引用 i 值的结果都将是3。
关于自由变量的取值来看两个例子:
1 var test = 10; 2 function fun() { 3 var test = 100; 4 return function foo(num) { 5 if (num < test) { 6 console.log(num + ‘<‘ + test); 7 } 8 } 9 } 10 var f1 = fun(); 11 f1(15); 12 //15<100
可以看到这个例子中test的取值是100,这个100来自于创建foo函数的作用域fun中,fun也是foo函数的父级函数;那么自由变量到底是来自创建它的作用域还是其父级作用域呢?在看一个例子:
1 var test = 10; 2 parameter = function (num) { 3 if (num > test) { 4 console.log(num+‘>‘+test); 5 } 6 }; 7 (function (fun) { 8 var test = 100; 9 fun(15); 10 })(parameter); 11 //15>10
上面这个例子中,test的取值为10,而这个10是来自全局作用域的。这证明了自由变量实际上是在创建这个函数的作用域中取值而不是其父级作用域中取值。