JavaScript函数作用域链和闭包的理解
很少写文章,文笔不好请大家凑合着看吧, 初学js,个人的理解和感悟, 表达不清楚和错漏的地方欢迎各位留言批评指正!
函数作用域链
作用域(也称词法作用域, 因为是在词法分析阶段创建的) 的概念几乎在所有编程语言当中都有, 比如C语言中每个{} 包围的块就是一个独立的作用域(块域). 在JavaScript中没有块级作用域, 只有全局作用域 和 函数作用域, 当一个函数没有子函数的时候, 这非常好理解. 但是嵌套函数的出现带来了非常微妙的作用域链规则.
作用域链是在函数定义时创建的, 在运行的时候可以修改, 比如try...catch语句, with语句等, 都可以修改作用域链, 然后在作用后恢复.
调用函数的时候会创建一个 execution context, 函数是在这个上下文中进行, 执行函数时, 这个上下文会被压栈, 调用结束之后会弹栈销毁. 这个上下文变量中就保存了函数的作用域链和其他执行环境. 作用域链本质上就是一个对象列表, 它保证函数在执行时能够按照规则依次访问作用域中的变量和函数.
//定义函数 function func(a,b){ var s = a+b; return s; } //调用这个函数 func(3,4);

定义函数func()时, js引擎会为func()创建一个作用域链对象(类似于execute context对象), 将global object保存到这个对象中; 当调用func()时, 就会创建一个执行上下文对象(execute context)对象, 其中的scope属性就保存执行上下文的作用域链. 作用域链维护一个有序的对象列表, 当需要解析函数中用到的变量和函数时就依次查找这个对象列表中的各个对象的各个属性, 找到就返回属性的值, 遍历所有对象都没有找到就返回 undefined, 这种有序保证了局部变量会先于同名全局变量被找到.
当有嵌套函数时, 最内层函数的作用域链中还包含有包含函数的外层函数的作用域对象, 位置在活动对象最后, 在全局对象之前, 依据函数嵌套规则, 层层外推.
了解了作用域链的基本规则之后, 来学习闭包.
闭包
先看一段 JavaScript权威指南上的一段代码:
var scope = "global scope"; function checkscope(){ var scope = "local scope"; function f(){ return scope; } return f; } console.log(checkscope()()); // "local scope"
checkscope()返回的是一个函数对象f, 接着调用 f . 根据上文提到的词法作用域链规则, 很明显, f() 沿着作用域链直接找到的是活动对象(active object)中的scope变量, 因为f()的作用域链里保存了这个变量, 至于checkscope() 调用返回后这个变量有没有被销毁我们暂时不管.
定义: <<JavaScript权威指南>>中描述: 函数对象可以通过作用域链相互关联起来, 函数体内部的变量都可以保存在函数作用域内的特性.
上面代码中的 f() 以及f() 的作用域链的总和就可以理解为是闭包, 它是JavaScript中的一个强大的特性, 简言之: 它可以捕捉局部变量和参数, 并一直保存下来, 看起来像是这些变量绑定到了在其中定义它们的外部函数.(出自<<JavaScript权威指南>>). 这例子中就是将scope变量绑定到了checkscope()中, 并且一直保存下来, 没有随着checkscope()的调用返回而被销毁.
为什么scope没有被销毁呢? 其实很简单, 因为词法作用域链. scope是checkscope()的局部变量, 保存在checkscope()的函数调用栈中, 当checkscope()调用返回时, 调用栈的所有变量都应该被销毁, 但是, 如果将scope换成是一个对象呢? 你就知道scope只是一个引用, 真正的对象放在栈区, scope只是指向了这个对象. 这个栈区的对象是由gc(垃圾回收器)管理的, 是否应该被销毁时要看引用计数是否为0, 很明显f() 的词法作用域链引用了它, 所以它仍然存在.
现在将对象换成变量, 可以有两种方式理解: 一, js将这个对象也包装成对象进行保存, 二, js会检查作用域链, 将它复制一份保留了. 总之这样看起来就是这个局部变量被永久保存起来了, 只要有地方可能引用到它, 它就不会被gc回收.
下面引用<<JavaScript权威指南>>的部分解释, 但是我还不是完全理解, 欢迎各位赐教:
We described it as a list of objects, not a stack of bindings. Each time a JavaScript function is invoked, a new object is created to hold the local variables for that invocation, and that object is added to the scope chain. When the function returns, that variable binding object is removed from the scope chain. If there were no nested functions, there are no more references to the binding object and it gets garbage collected. If there were nested functions defined, then each of those functions has a reference to the scope chain, and that scope chain refers to the variable binding object. If those nested functions objects remained within their outer function, however, then they themselves will be garbage collected, along with the variable binding object they referred to. But if the function defines a nested function and returns it or stores it into a property somewhere, then there will be an external reference to the nested function. It won’t be garbage collected, and the variable binding object it refers to won’t be garbage collected either.
简单翻译过来: 每次调用函数 func() 是都会为这次调用创建一个对象 binding-obj (保存局部变量), 并将 binding-obj 加入到作用域链 scope-chain 中.当函数返回时:
如果没有嵌套函数, 那么 binding-obj 就会从 scope-chain 中被移除.
如果存在嵌套函数 subfunc, 那么每个嵌套函数的 sub-scope-chain 就会有一个指向这个 scope-chain 的引用, 而scope-chain 中保存了 binding-obj.
- 如果这些嵌套函数没有被保存在func()中(保存在属性中或者内部其嵌套函数调用它), 那么也会被回收.
- 如果嵌套函数作为返回值或被保存在某个属性中 , 那么func() 调用返回后, binding-obj 将不会被回收, 局部变量得以保存下来.
举个例子: 下面代码中, method1()不会在ClassA()调用之后被回收, 因为method3()中使用了它, 而method2() 就只是一个没用的函数. 当然, 如果ClassA是不作为构造函数,在最后一行 加一句 : return method2; 那么 method2()也会被保存下来, 因为其他地方有引用到这个对象(返回值).
function ClassA(){ //定义构造函数 var method1 = function(){ //内部函数, 在外部不能访问 console.log("method1"); }; var method2 = function(){ console.log("method2"); }; this.method3 = function(){ //为新创建的实例绑定方法 console.log("method3"); method1(); // 调用method1(),闭包使得method1()可以保存下来, 即在构造函数调用之后还能继续使用. } }; var instance = new ClassA(); instance.method3();
下面来看两个例子来巩固一下:
第一个例子引用了 阮一峰 老师的博客, 点击查看
var name = "The Window"; var object = { name : "My Object", getNameFunc : function(){ return function(){ return this.name; } } } console.log(object.getNameFunc()()); // "The Window" console.log(this.name); // "The Window" var name = "The Window"; var object = { name : "My Object", getNameFunc : function(){ var that = this; return function(){ return that.name; }; } }; console.log(object.getNameFunc()()); // "My Object" /* 我的理解: * object.getNameFunc()返回一个函数对象, 后面加()调用这个函数. * 先理解后面一种形式: object调用getNameFunc时this对象指向的是object,传递给getNameFunc. getNameFunc将this保存到了that变量中, 返回了一个函数对象, * 函数对象的作用域链里的活动对象里保存了外部函数(即getNameFunc()的所有局部变量),当然也就保存了that变量(指向object),所以that.name沿着作用域链 * 就找到了object.name ("My Object") * 再看前一种形式: object.getNameFunc调用时,this(->object)保存在了getNameFunc()的活动对象里,返回了一个函数对象, 这个函数对象的作用域链的活动对象里 * 面包含了getNameFunc()的所有局部变量和参数(注意: this变量是保存在作用域链的全局对象中的).接着就调用内部函数, 沿着作用域链寻找 * this变量, 在活动对象中每找到,然后去找全局对象(->window), 所以找到window.name ("The Window"). */
第二个例子参考了 这篇文章
//经典错误! 使用setTimeout()也会常常遇到这个问题. //绑定到对象上每次alert的值都是for循环执行完之后得到了length值. 而不是实时得到的值. for(var i=0;i<elements.length;++i){ elements[i].onclick = function(){ alert(i); } } //使用立即调用来避免这个问题 for(var i=0;i<elements.length;++i){ (function (n){ elements[n].onclick = function(){ alert(n); } }(i)); }

浙公网安备 33010602011771号