Javascript Closures (3)

在前两篇文章 Javascript Closures (1)Javascript Closures (2) 中,介绍了很多有关 Javascript 内部机制的东西,下面我们就开始详细研究一下闭包。

注:所有的东西都参考自这篇文章:Javascript Closures

自动垃圾回收

ECMAScript 使用了自动垃圾回收机制,不过标准中并没有指出具体细节。一个普遍的看法是当一个对象不在被其它对象或者变量引用的时候,它就会成为垃圾回收的对象:在某个时间被摧毁并把它所占用的资源都释放掉。这个机制对任何执行环境中的对象都适用,包括函数对象、原型链上的对象等。

闭包的形成

当从另一个函数内部创建的函数对象被返回并赋值给某个对象的属性(或者全局变量、外部函数的参数等)的时候,闭包就产生了。(这句话有点别扭,英文原文是这样的:A closure is formed by returning a function object that was created within an execution context of a function call from that function call and assigning a reference to that inner function to a property of another object. )看例子吧:

1
2
3
4
5
6
7
8
9
10
11
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 所指向的函数对象已经有了一个 [[scope]] 属性,它指向函数对象在创建时的执行环境中的一个scope链(其中包括Activation/Variable 对象),所以当时产生的Activation/Variable 对象也不会被自动回收。这样就形成了一个闭包,到这里大家应该可以领会到了,闭包的奇妙之处在于虽然对外部的函数调用虽然已经结束,但是却不止返回了创建的内部函数对象,还把对象被创建时候的“状态”一并打包返回了! 举个例子来说,它就好像一个电脑制造厂,允许我们定制一些东西,比如CPU型号、内存大小等。当我们把自己的定制单——也就是参数列表——传给电脑制造厂之后,他就会给你一台保留了你想要的CPU型号、内存大小——也就是参数的值——的电脑。拿上面的代码例子来说,虽然外部函数执行完毕了,但是返回的函数对象的Activation/Variable对象(姑且称其为 ActOuter1)上的属性localVar, arg1 和 arg2 却还是保留了当初的值:8, 2 和 4. 假设再调用一次外部函数 exampleClosureForm :

1
var secondGlobalVar = exampleClosureForm(12, 3);

那么就又形成了一个新的闭包,secondGlobalVar 所指向的函数对象中的Activation/Variable对象(姑且称其为 ActOuter2)上的属性 arg1 和 arg2 的值变成了 12 和 3 。大家可以看到内部函数 exampleReturned 其实是有四个参数的:arg1 , arg2 ,innerArg, localVar ,这么多参数在闭包中是怎么找到值的呢?

假设我们现在调用了 globalVar(2), 执行之后就会生成一个新的Activation/Variable对象(称其为 ActInner1),因为传送进来了一个参数2,所以ActInner1就有了一个值为2的属性 innerArg 。这个对象会添加到执行的函数对象的scope链的头部,就形成了一个新的scope链: ActInner1 -> ActOuter1 -> global 。前面已经详细介绍了标识符的定位方式了,所以当它看到有四个参数的时候就跑到 scope 链上去查,先查 ActInner1 ,发现上面有一个 innerArg=2 ,其他就找不到了,于是接着找 ActOuter1,发现了三个, arg1=2,arg2=4,localVar=8 。好了,这下四个都找齐了,就不再往下找了,直接返回 (2 + 4)/(2 + 8) 。

以上就是 ECMAScript 内部函数的实现原理和机制。由于内部函数所创建的对象的 [[scope]] 属性指向的 scope 链中包含了当时执行环境中所创建的Activation/Variable对象,所以只有当所有指向这个函数对象的引用都被释放了之后才会进行相应的垃圾回收。

另外,在内部函数中也可以有内部函数,也就是说,内部函数是可以嵌套存在的。那样形成的 scope 链也会越来越长。尽管 ECMAScript 的定义中说 scope 链的长度是有限的,但是却没有具体规定长度到底是多少,所以内部函数的嵌套层数貌似是可以由开发者自己来决定。

闭包有什么用?

咱们用例子说话:

例1. setTimeout 调用闭包

1
2
3
4
5
6
7
8
9
10
11
function callLater(paramA, paramB, paramC){
    return (function(){
        paramA[paramB] = paramC;
    });
}
 
...
 
 
var functRef = callLater(elStyle, "display", "none");
hideMenu=setTimeout(functRef, 500);

上面这个例子中是先用闭包定制了一个 functRef,然后用 setTimeout 调用它。很多应用中我们可能不知道要隐藏哪个 Menu,用闭包来实现就很灵活。

例2. 将函数和对象实例的方法关联起来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function associateObjWithEvent(obj, methodName){
    return (function(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.
}

有时候我们可能会需要在一些DOM元素上触发事件,但是我们事前并不知道要在哪些元素上触发,也不知道触发的是哪些事件,这个时候就可以用闭包将事件处理函数和DOM元素联系起来。上面的例子中就给我们演示了如何实现:每次给一个元素的某个事件添加处理函数的时候,都用闭包来产生一个,然后在后面再编写处理函数的具体实现。

例3.封装功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
var getImgInPositionedDivHtml = (function(){
    var buffAr = [
        '<div id="',
        '',   //index 1, DIV ID attribute
        '" style="position:absolute;top:',
        '',   //index 3, DIV top position
        'px;left:',
        '',   //index 5, DIV left position
        'px;width:',
        '',   //index 7, DIV width
        'px;height:',
        '',   //index 9, DIV height
        'px;overflow:hidden;\"><img src=\"',
        '',   //index 11, IMG URL
        '\" width=\"',
        '',   //index 13, IMG width
        '\" height=\"',
        '',   //index 15, IMG height
        '\" alt=\"',
        '',   //index 17, IMG alt text
        '\"><\/div>'
    ];
 
    return (function(url, id, width, height, top, left, altText){
 
        buffAr[1] = id;
        buffAr[3] = top;
        buffAr[5] = left;
        buffAr[13] = (buffAr[7] = width);
        buffAr[15] = (buffAr[9] = height);
        buffAr[11] = url;
        buffAr[17] = altText;
 
        return buffAr.join('');
    }); 
})();

上面这个例子封装了一个拼装字符串的功能,其实这个功能也可以换其他的方式实现。但是如果直接写一个本地函数并把buffAr在里面声明的话,那么每调用一次就会重新创建一次这个数组;如果用全局变量的话又会让代码变得难以管理。用闭包把这个功能封装起来之后就没有这些顾虑了,里面的那个 buffAr 只会创建一次,但是却一直可以使用。

其他例子:

比较有名的是 Douglas Crockford 写的 Private Members in JavaScript ,可以看一下。

关于闭包的用途还有很多,了解它的实现方式会帮助你认识到该怎么使用它。

意外的闭包

很多时候我们可能不想创建闭包,但是在代码中却创建了。这时候我们不但不能利用到闭包的好处,还会带来一些诸如IE内存泄露等问题,也会对代码效率产生影响。看一下下面的代码:

1
2
3
4
5
6
7
8
9
var quantaty = 5;
function addGlobalQueryOnClick(linkRef){
    if(linkRef){
        linkRef.onclick = function(){
            this.href += ('?quantaty='+escape(quantaty));
            return true;
        };
    }
}

当调用addGlobalQueryOnClick的时候就会产生一个新的内部函数,如果它被调用很多次的话就会产生很多不同的函数对象。解决办法就是把那个内部函数提出来放在外面:

1
2
3
4
5
6
7
8
9
10
11
var quantaty = 5;
function addGlobalQueryOnClick(linkRef){
    if(linkRef){
        linkRef.onclick = forAddQueryOnClick;
    }
}
 
function forAddQueryOnClick(){
    this.href += ('?quantaty='+escape(quantaty));
    return true;
}

这样无论addGlobalQueryOnClick被调用多少次,forAddQueryOnClick函数也只会创建一个实例。还有一种常见的产生意外闭包的情况是在构造函数中:

1
2
3
4
5
6
7
8
9
10
11
12
13
function ExampleConst(param){
    this.method1 = function(){
        ... // method body.
    };
    this.method2 = function(){
        ... // method body.
    };
    this.method3 = function(){
        ... // method body.
    };
 
    this.publicProp = param;
}

这样每次用构造函数创建一个新的对象,内部的函数就会创建新的实例,很不环保。解决方法就是使用 prototype:

1
2
3
4
5
6
7
8
9
10
11
12
function ExampleConst(param){
    this.publicProp = param;
}
ExampleConst.prototype.method1 = function(){
    ... // method body.
};
ExampleConst.prototype.method2 = function(){
    ... // method body.
};
ExampleConst.prototype.method3 = function(){
    ... // method body.
};

IE 内存泄露问题

IE 浏览器(IE4-6,因为这篇文章出来的时候还没有IE7)的垃圾回收机制有一个缺陷,导致它对循环引用(比如A引用了B,B又引用了A)的ECMAScript对象和一些宿主对象(主要包括DOM节点和 ActiveX 对象)不会进行回收。一旦形成了循环引用,这些对象占用的内存就不会被释放,除非你关闭浏览器。

假设这样一种情况:A引用了B,B引用了C,C又引用了A。如果没有任何其他对象引用A,B,C,那么A,B,C就应该被自动挥手。但是在IE中,如果A,B,C中的一个是DOM节点或者 ActiveX,它就发现不了A,B,C是循环引用的。

闭包很容易就会形成循环引用,所以当你使用闭包的时候要考虑一下这个问题。

========= 华丽丽的分割线 ===============

关于Javascript 的闭包终于弄完了,通过总结分析那篇文章,我也算是对Javascript的内部机制和闭包的形成有了一个比较清晰的了解,学到了很多东西。以后工作重心慢慢转移到前端,要多写代码多看好文章才行了。

最近又有点浮躁了,要脚踏实地~哎。信春哥,无BUG!


本文链接:http://www.zhuoqun.net/html/y2009/1287.html 转载请注明出处。TrackBack:http://www.zhuoqun.net/html/y2009/1287.html/trackback

相关日志


Posted in Web Develop, 技术.

2条评论

  • At 2009.07.04 13:39, Qing said:

    我到现在也弄明白闭包

    • At 2009.10.13 11:48, kamal said:

      好文,正对原文的英文头痛呢,谢谢楼主的翻译总结分享

      (Required)
      (Required, will not be published)