看了大家的讨论,我也很感兴趣,所以特意去了MSDN了解了下IE解析器小组对闭包的解释,自己做了下研究。觉得挺有意思的,发出来给大家分享下。

该分析不仅仅适用js,凡是可以实现闭包的语言都存在相同问题。

何谓闭包:方法内的局部变量,可以在该方法执行完后(即该方法的作用域外部)被访问。
或者说:方法内局部变量的生命周期超过了方法本身的生命周期。

看js示例1:
function AdderFactory(y) {
  return function(x){return x + y;}
}
var MyFunc;
if (whatever)
  MyFunc = AdderFactory(5);
else
  MyFunc = AdderFactory(10);
print(MyFunc(123)); // Either 133 or 128.
第一点要牢记,js中一切都是对象,方法也是,属性也是,变量也是。

在上例中,方法(对象)AdderFactory内部创建了一个匿名方法(对象)function(x){return x + y;}。并且,我们利用return指令将该匿名方法的一个引用扔给AdderFactory方法外部使用,比如可以用于赋值。
如果变量whatever为true,则语句MyFunc = AdderFactory(5);执行完后,变量MyFunc就被赋予了AdderFactory对象中用return扔出的匿名对象的引用。
一般情况下,AdderFactory方法(对象)内部所有变量的生命周期都应该小于该方法的执行时间。但在示例中,由于MyFunc变量保持了对匿名对象的引用,所以该匿名对象在AdderFactory方法执行后不会被GC回收,又由于匿名对象是AdderFactory对象的一个属性,所以同时GC也不会回收AdderFactory对象。
以上示例代码执行完成后的对象引用图如下:

[attach]7395[/attach]

图中矩形和椭圆代表对象,箭头代表引用。

闭包的功能非常强大,上面的示例中也不会造成不好的影响。

但是,滥用闭包将十分可怕。

看js示例2:
<html>
<script language="JavaScript">
<!--
Function closureTest (){
        var maskDiv = document.createElement("div");
        maskDiv.id = "myDiv";
        maskDiv.style.width = "100%";
        maskDiv.style.height = "100%";
        maskDiv.style.position = "absolute";
        maskDiv.style.filter = "alpha(opacity=50)";
        maskDiv.style.backgroundColor = "red";
        maskDiv.oncontextmenu = function(){ return false; };
        document.body.appendChild(maskDiv);
}
//-->
</script>
<body onload=”closureTest();”>
</body>
</html>

为了突出主题,我省略了不影响示例的多余标签。
用IE执行一个如上的页面,打开windows任务管理器,查看iexplore.exe进程的“内存使用”。
按几下F5看看内存是如何增加的。^_^
这就是闭包导致内存泄露问题。

看分析:
这是一个闭包应用。为什么?
看这里,看这里,看这里
maskDiv.oncontextmenu = function(){ return false; };

为了说明问题,看下图:

[attach]7396[/attach]

显然,这里出现了循环引用(实箭头是直接引用,虚箭头是间接引用)。为什么?
我们看看程序执行过程:
①.        页面载入完成后IE调用body的onload事件的绑定方法closureTest(),并以该方法为构造器创建closureTest对象。

然后执行var maskDiv = document.createElement("div");语句。该语句有下面3步,
②.        通过document.createElement("div")方法生成一个div对象,对象id在之后赋予。
③.        通过var maskDiv语句生成一个closureTest的内部变量(对象)maskDiv。
④.        通过“等号”= 赋值,将一个对新建div的引用存入maskDiv变量。

一直到maskDiv.style.backgroundColor = "red";语句都是对div对象的设置,与本例无关。
然后执行maskDiv.oncontextmenu = function(){ return false; };语句。该语句也有下面3步,
⑤.        在closureTest对象内创建一个匿名对象Anonymity,其构造器是function(){ return false; }方法。
⑥.        在IE中,也许oncontextmenu属性是div原有的属性(对象),也许不是,但不管如何,maskDiv.oncontextmenu语句保证myDiv对象必定有该属性(有则用之,无则创建)。
⑦.        通过“等号”= 赋值,将匿名对象的引用存入oncontextmenu对象中。

显然,例2是比例1略微复杂的一个闭包应用。
本来,光是实箭头还不至于产生循环引用,但是由于闭包的使用,导致Anonymity与maskDiv对象出现了间接引用。因为在closureTest()方法执行完后,由于图中⑦的引用的存在,Anonymity对象将继续快乐的生活着,所谓“一佛得道,鸡犬升天”。closureTest对象也要沾点喜气啦,而maskDiv变量的长寿也就顺理成章罗。如此循环往复,无穷尽也。

那么,如果顺其自然,IE何时才会结束他们的圈地运动呢?
答案是关闭当前页面所处的IE窗口时。
但是,可恶的是,如果你是在某些IDE内使用预览功能看页面的话,由于IDE替代IE接管了内存管理,所以,圈地运动的结束被迫延迟到了,关闭IDE时,比如EditPlus。
这太恐怖了,我要手工干掉这些乱炒房地产的开发商。OK,没问题,我们有几十种方法,但为了方便,举例4种典型方法如下:

方法一:移花接木
这是常用的解决办法,将
var maskDiv = document.createElement("div");
语句前的var定义去掉,maskDiv对象将不属于closureTest对象,变成了一个全局变量。
变化后的引用关系图如下:

[attach]7397[/attach]

很显然,圈地运动不可能发展起来了。但其还是有点不尽如人意。
1.        maskDiv是全局变量,当页面复杂时(比如引入了多个js文件),随意污染window对象的全局作用域,将陷入名称空间碰撞的泥潭。
2.        自然情况下,这些对象及引用一旦创建,则只能在页面卸载时被GC回收,有点浪费内存空间。——我们可是有名的铁公鸡。

方法二:战争践踏
在脚本中增加如下代码:
function myGC(){
        document.body.removeChild(document.getElementById("myDiv");
}
在body标签上增加onunload事件处理 onunload=” myGC();”
这有些暴力了,在卸载页面时,我们强行将myDiv给干掉,循环引用自然被打破,皮之不存,毛将焉附!
但是,它的缺点也是明显的,
1.        增加了庞大的代码量
看人家方法一,不但没有增加代码,还去掉了个var,咱可到好,一个div就多写这么多代码,那复杂点的页面还不把人给烦死。
2.        增加了代码耦合
myDiv这个名字这么好听,我closureTest就想自己留着用,凭啥要告诉你个myGC。知道个名字也就算了,还要知道我把她嫁到了body家,郁闷那。

方法三:温柔一刀
比方法二温柔一点,既然不让干那么野蛮的事情,我们把方法二中的
document.body.removeChild(document.getElementById("myDiv");
修改为
document.getElementById("myDiv").oncontextmenu = null;
在页面卸载时,将引用图中的⑦给一刀斩断。不要把问题扩大化,应该在局部解决。
但是,本质上,方法三并不比方法二好很多,庞大的代码量和强耦合还是我们的心头刺。

方法四:Oh,my god! 上帝说,“这很简单”!
我们只需要在
document.body.appendChild(maskDiv);
语句后增加一句话,一切烦恼都烟消云散。
maskDiv = null;
真是太完美了,优点如下:
1.        只是简单的增加了几个字符,
2.        他不会在window对象下增加危险的全局变量,不会出现无聊的命名冲突,
3.        在closureTest()方法执行后,引用图中的③、④就乖乖的让出内存地盘,真应了偶铁公鸡的名号。(一些小气的GC还是不闻不问)
4.        没有增加讨厌的myGC()方法,不需要在body上再绑定onunload事件。赞美神!少敲好多代码啊。
5.        myDiv这名字就我closureTest自己用了,减少了可能的耦合点,赞!


神说:“简单就是美!”

综上所述,闭包之所以容易引起内存泄露,本质是由于其容易引起循环引用。所以,如何发现并避免出现循环引用是我们要关注的重点。不要被所谓闭包的新名词遮蔽我们的双眼。