论坛首页 Web前端技术论坛

(译)理解 JavaScript 闭包

浏览 8483 次
精华帖 (0) :: 良好帖 (5) :: 新手帖 (0) :: 隐藏帖 (0)
作者 正文
   发表时间:2011-12-25   最后修改:2011-12-27

前言:

理解JavaScript闭包——Javascript Closures是一篇经典文章。网上(包括iteye)有翻译的中文版本,但是有一个部分并未翻译。在学习的过程中,我决定翻译下来,让这篇经典文章有一个完整的中文版。基于自己是第一次翻译,肯定存在一些错误,一些部分采用了意译。翻译之后,对译文进行了三遍润色和修改,希望大家提出意见,继续改进这篇译文。

附件中是这篇文章的完整翻译版,已经整合进下面的内容,其他部分来自网络,在此感谢之前翻译的作者。

最后,希望能给大家带来写帮助。

 

正文

标识符解析
标识符的解析依赖于作用链。ECMA262倾向于把this划归为关键词而不是标识符,解析总是依赖执行环境中使用的this的值而不是依赖对作用链,因此标识符的解析不是那么的合理。(译者注:含有this的情况)
Example 1:

 

 

        var example = "CaoLixiang";
        var outer = function() {
            var example = "HuJin";
            return function() {
                var example = "Love";
                alert(this.example);
            }
        }
        var test = outer();
        test(); // CaoLixiang

 

对“example”这个标识符的解析,依赖于this的值,指向全局对象,而不是作用链。调用test()时的作用链为:内部匿名函数的活动对象->outer的活动对象->全局对象,如果依赖作用链,将返回“Love”。
(译者注结束)
标识符解析从作用链的第一个对象开始。需找与标识符相对应的属性。因为作用链是一个对象链,如果在第一个对象对应的作用链中无法找到对应的属性,会继续在作用链中的第二个对象中寻找。
以此类推,在原型链的一个个对象中寻找是否存在与标识符对应的属性,直到作用链被穷尽。
(译者注)
Example 2:

 

        var age = 200;
        var sec = function() {
            var age = 100;
            return function() {
                var age = 22;
                alert(age);
            };
        };
        var test = sec();
        test(); // 22
         
        var age = 200;
        var sec = function() {
            var age = 100;
            return function() {
                alert(age);
            };
        };
        var test = sec();
        test(); // 100

        var age = 200;
        var sec = function() {
            return function() {
                alert(age);
            };
        };
        var test = sec();
        test(); // 200
   Object.prototype.age = 1000;
    var sec = function() {
        return function() {
            alert(age);
        };
    };
    var test = sec();
    test(); // 1000

 
(译者注结束)
标识符的操作与标识符访问方式一样。作用链中有对应的属性的对象会成为标识符访问的对象,标识符将作为那个对象的属性名。注意:全局对象总是在作用链的最后端。
在执行环境中进行函数调用时,会将活动对象置于作用链前端,高效地进行一些检测操作:函数体中是否有对应的属性;内部函数是否声明了命名属性或者本地变量,所有这些将会被解析为活动对象中的命名属性。

闭包

垃圾自动回收机制
ECMA262有一个垃圾回收机制,语言标准中并未规定其实现的细节。因此有多种不同方式的实现方式,但其中一些实现的优先级很低。
通常认为当一个对象不被引用(正在执行的代码无法再引用),它就变为可被回收的状态,并且在未来某一个时间点上被销毁,它消耗的资源会被释放以供系统复用。
垃圾回收一般出现在执行环境中,作用链中,活动对象以及执行环境所创建的其他对象,当然也包括函数对象,当无法被访问时,就会置为可被回收的状态。

创建闭包
执行环境中一个函数被调用,在调用中,将一个返回它的内部函数对象的引用,将这个引用赋给另外一个函数的属性,就会形成闭包。也可以直接将这个函数对象的引用指定给,比方说,全局变量,或者一个全局课访问的对象的属性,或者按引用传递作为外部函数调用的一个参数。

function exampleClosureForm(arg1, arg2){
    var localVar = 8;
    function exampleReturned(innerArg){
        return ((arg1 + arg2)/(innerArg + localVar));
    }
    /* 返回对内部函数exampleReturned的引用 */
    return exampleReturned;
}

var globalVar = exampleClosureForm(2, 4);

 
当exampleClosureForm被调用时,执行环境中所创建的函数对象(exampleReturned)不能被垃圾收集机制收集,因为它被一个全局变量(globalVar)所引用,并且可以被调用、执行。
如 :
globalVar(N);
可能有一点复杂,被globalVar所引用的函数对象(exampleReturned)(其作用域链包含了创建它的函数——exampleClosureForm的活动对象,当然也包括全局对象)。活动对象无法被垃圾收集机制收集掉,因为globalVar所引用的函数对象(exampleReturned)需要在每次调用时,在其作用域链中添加它。
一个闭包就此形成。内部函数变量和作用链中的活动对象绑定起来作为执行环境。
活动对象被分配到globalVar的作用域链中。 globalVar现在被作为全局对象一个属性。活动对象的状态及其属性值会被保存。调用内部函数时将会解析活动对象的标识符,活动对象中具有相应标识符的属性将会被作为内部函数的属性,即使用于创建它的执行环境已经退出,这些属性的值仍然可以读取和设置。
上面的例子中,当外部函数返回时(推出其执行环境后),活动对象拥有:arg1,属性的值为2;arg2,属性的值为4;localVar,属性的值为8,并且exampleReturned是一个引用,指向从外部函数中返回的函数对象。(为了方便后面的讨论,我们将这个活动对象记为”ActOuter1”)
如果exampleClosureForm函数被再次以下面的形式调用:

 

var secondGlobalVar = exampleClosureForm(12, 3); 

一个新的执行环境将被创建,同时伴随一个新的活动对象,一个新的函数对象也会被返回,不同的[[scope]]属性将会是一个含有这个活动对象的作用链,这个活动对象包含:属性arg1,其属性值为12;arg2,其属性值为3。(为了方便后面的讨论,我们将这个活动对象记为”ActOuter2”)
另一个闭包通过第二次exampleClosureForm的调用就此产生。
分别将exampleClosureForm调用后产生的两个函数对象的引用赋予全局变量globalVar 和 secondGlobalVar,会返回表达式((arg1 + arg2)/(innerArg + localVar)),这个表达式针对4个标识符进行一些操作。如何解析这些标识符是闭包作用和价值所在。
考察一下globalVar所指向的函数的执行,如globalVar(2)。新的执行环境和活动对象被创建(我们把它称作”ActInner1”),它会被添加到函数执行时[[scope]]属性所指向的作用域的前端。
ActInner1提供一个称为”innerArg”的命名属性,参数值 2将会被赋予给它。
新的执行环境的作用链将是 ActInner1 -> ActOuter1 -> global object。
标识符解析依靠作用链返回表达式((arg1 + arg2)/(innerArg + localVar))完成。这些标识符的值通过检测属性决定:即逐一检查作用域链上的每一个对象,检测是否有对应名称的标识符。
作用链中的第一个活动对象是ActInner1,拥有一个innerArg的值为2的属性。其他的3个标识符可以在ActOuter1中找到:arg1的值为2,arg2的值为4,localVar的值为8.函数调用返回((2 + 4)/(2 +8)。
比较secondGlobalVar的执行,如secondGlobalVar(5)。把新的活动对象称作ActInner2,活动链为:ActInner2-> ActOuter2-> global object。ActInner2返回值为5的innerArg属性,值为12,3,8的arg1,arg2和localVar通过ActOuter2返回。最终返回的值为((12 + 3)/(5 +8)。
再一次执行secondGlobalVar,另一个新的活动对象就会出现在作用链前端,但是ActOuter2仍然是紧随最前端的多动对象,可以再一次解析到arg1, arg2 和 localVar这些值。
这就是ECMAScript内部函数如何读取,保存,访问形式参数,内部变量和它们的执行环境中的本地变量的原理。这也是为什么一个形成的,还继续存在的闭包能够让这样一个函数对象保存这些值的引用,读取和改写它们的原因。 内部对象创建时的执行环境中的活动对象,会一直在作用链之上,可以被函数对象的[[scope]]属性引用到,直到全部引用被释放并且函数对象被垃圾回收机制标识为可以进行垃圾回收。(同时包括其作用链上一些不会再用到的对象)
内部函数也可以拥有它们自己的内部函数,通过执行函数返回的内部函数形成背包也会返回内部函数并且形成它们自己的闭包。每一个嵌套的作用链获得了额外的活动对象,这些活动对象源于执行环境中内部函数的创建。ECMAScript规范要求作用链的实现是完善的,但是没有对作用链的长度加以限制。各种不同的实现可能有一些欺骗和限制,但是还没有特别严重的现象被披露出来。嵌套内部函数的潜力到目前为止超出了使用它们的人的期待。

通过闭包可以做什么
如果回答——显然可以做所有事,任何事情,似乎有些奇怪。有人告诉我闭包可以使得ECMAScript模拟一切,只需要你懂得如何实现和构思这些模拟。或许有一些难懂但是最好还是通过一些例子开始。

例1.为函数引用设置延时
闭包一个常见的作用是为一个优先于另一个函数的执行的函数提供参数。例如,当一个函数被作为setTimeout这个函数的第一个参数。
setTimeOut会调用一个作为其第一个参数的函数(也可以是一个javascript字符串形式的源代码,但是这里不加以讨论),作为其第一个参数,之后,是一个以毫秒表示的时间间隔(作为第二个参数)。如果一段Js代码使用到setTimeOut,它会调用setTimeOut函数然后传递一个函数对象的引用作为第一个参数以及一个毫秒数的间隔作为第二个参数,但是仅仅作为一个函数对象的引用,不能为其提供参数。
可以通过调用一个函数,返回其内部函数对象的引用,把这个引用传递给setTimeOut函数。内部函数执行所用到的参数会在外部函数被调用时加以返回。setTimeOut执行时避免了参数的传递,但是内部函数仍然可以访问到调用它的外部函数的参数。

function callLater(paramA, paramB, paramC){
    /* 返回一个匿名内部函数的引用,由一个函数表达式构成 */       
   return (function(){
        /* 这个内部函数被setTimeOut调用执行;当它执行时可以读取、操作外部函数传递的参数。 */         
   paramA[paramB] = paramC;
    });
}

/* 调用函数后将返回一个内部函数的引用。传递的参数将可以被内部函数读取和使用。*/ 
var functRef = callLater(elStyle, "display", "none");
/* 调用setTimeout函数, 传递内部函数的引用赋值给 functRef ,以functRef作为setTimeOut的第一个参数变量
*/ 
hideMenu=setTimeout(functRef, 500);

 

例2.通过对象实例方法关联函数
当一个函数对象的引用在未来的某些时刻可以被执行,传递参数在执行开始的时候不会被访问到,直到赋值的时候确定下来,这个特性很有用的。
为交互特别是针对DOM元素的交互封装的函数对象是一个很好的例子。它拥有doOnClick,doMouseOver,doMouseOut这些方法,当对应事件在元素上被触发时,相应的方法期望被执行。
下面例子使用一个基于关联了元素事件操作句柄的对象实例的闭包。使得事件句柄能够调用对象实例的相应方法,传递的事件对象和一个关联元素的引用将会以一个引用返回。

function associateObjWithEvent(obj, methodName){
/* 返回的内部函数将被作为一个DOM元素的事件句柄 */
     return (function(e){
        /* 事件对象将被传递作为e参数 */
         e = e||window.event; // 浏览器差异性
                    return obj[methodName](e, this);
    });
}
function DhtmlObject(elementId){
var el = getElementWithId(elementId);
if(el){
el.onclick = associateObjWithEvent(this, "doOnClick");
               el.onmouseover = associateObjWithEvent(this, "doMouseOver");
               el.onmouseout = associateObjWithEvent(this, "doMouseOut");
           }
}
DhtmlObject.prototype.doOnClick = function(event, element){
    // doOnClick method body.
}
DhtmlObject.prototype.doMouseOver = function(event, element){
    // doMouseOver method body. 
}
DhtmlObject.prototype.doMouseOut = function(event, element){
    // doMouseOut method body. 
}

 
DhtmlObject的任何实例不需要知道内部细节也不会破坏全局命名空间或者是与其他DhtmlObject发生冲突。

 

   发表时间:2011-12-26  
楼上这个解释很深刻,以后多多向你学习。我把最近几个讨论闭包的例子都收集起来了,供大家参考。
http://www.iteye.com/topic/1119280
http://www.iteye.com/topic/1118705
http://www.iteye.com/topic/1118236
0 请登录后投票
   发表时间:2011-12-27  
caolixiang 写道


 

      
        var age = 200;
        var sec = function() {
            age = 100;
            return function() {
                alert(age);
            };
        };
        var test = sec();
        test(); // 200
 

这个答案是错误的。test()返回的是100,而不是200啊!这个问题很严重啊!

0 请登录后投票
   发表时间:2011-12-27  
lib 写道
caolixiang 写道


 

      
        var age = 200;
        var sec = function() {
            age = 100;
            return function() {
                alert(age);
            };
        };
        var test = sec();
        test(); // 200
 

这个答案是错误的。test()返回的是100,而不是200啊!这个问题很严重啊!


楼主疏忽了,主要想反映 在原型链的一个个对象中寻找是否存在与标识符对应的属性,直到作用链被穷尽。 代码改为: var age = 200; var sec = function() { return function() { alert(age); }; }; var test = sec(); test(); // 200 再附上一个例子
0 请登录后投票
   发表时间:2011-12-27  
panduozhi 写道
楼上这个解释很深刻,以后多多向你学习。我把最近几个讨论闭包的例子都收集起来了,供大家参考。
http://www.iteye.com/topic/1119280
http://www.iteye.com/topic/1118705
http://www.iteye.com/topic/1118236



+1
0 请登录后投票
   发表时间:2011-12-28  
lib 写道
caolixiang 写道


 

      
        var age = 200;
        var sec = function() {
            age = 100;
            return function() {
                alert(age);
            };
        };
        var test = sec();
        test(); // 200
 

这个答案是错误的。test()返回的是100,而不是200啊!这个问题很严重啊!

 

楼主的文章我没看,但是冰哥你这个是有问题的。

你这样写虽然产生了闭包,但是age并不在一个理想的闭包空间内。随着

var test = sec(); 

这步赋值操作,先是把全局变量的age重新赋值,然后再创建一个闭包。如果这段代码的最外层不是全局变量,影响的即为上级作用域,也就是说没有把age“闭”在闭包内。

说起来太抽象,你理解下这段代码

 

var age = 200;
var sec = function() {
    age = 100;
	tt();
    return function() {
        alert('3) '+age);  
    };
};
var tt = function (){
  alert('2) '+age)
}
alert('1) '+age)
var test = sec();  
test();
alert('4) '+age)
 
0 请登录后投票
   发表时间:2011-12-29  
日,404,你懂得
panduozhi 写道
楼上这个解释很深刻,以后多多向你学习。我把最近几个讨论闭包的例子都收集起来了,供大家参考。
http://www.iteye.com/topic/1119280
http://www.iteye.com/topic/1118705
http://www.iteye.com/topic/1118236

0 请登录后投票
   发表时间:2012-01-05  
+1 不错
0 请登录后投票
论坛首页 Web前端技术版

跳转论坛:
Global site tag (gtag.js) - Google Analytics