1、给DOM对象添加的属性是一个对象的引用。范例:
var MyObject = {};
document.getElementById('myDiv').myProp = MyObject;
解决方法:
在window.onunload事件中写上: document.getElementById('myDiv').myProp = null;
2、DOM对象与JS对象相互引用。范例:
function Encapsulator(element) {
this.elementReference = element;
element.myProp = this;
}
new Encapsulator(document.getElementById('myDiv'));
解决方法:
在onunload事件中写上: document.getElementById('myDiv').myProp = null;
3、给DOM对象用attachEvent绑定事件。范例:
function doClick() {}
element.attachEvent("onclick", doClick);
解决方法:
在onunload事件中写上: element.detachEvent('onclick', doClick);
4、从外到内执行appendChild。这时即使调用removeChild也无法释放。范例:
var parentDiv = document.createElement("div");
var childDiv = document.createElement("div");
document.body.appendChild(parentDiv);
parentDiv.appendChild(childDiv);
解决方法:
从内到外执行appendChild:
var parentDiv = document.createElement("div");
var childDiv = document.createElement("div");
parentDiv.appendChild(childDiv);
document.body.appendChild(parentDiv);
5、反复重写同一个属性会造成内存大量占用(但关闭IE后内存会被释放)。范例:
for(i = 0; i < 5000; i++) {
hostElement.text = "asdfasdfasdf";
}
这种方式相当于定义了5000个属性!
解决方法:
其实没什么解决方法:P~~~就是编程的时候尽量避免出现这种情况咯~~
说明:
1、以上资料均来源于微软官方的MSDN站点,链接地址:
http://msdn.microsoft.com/librar ... e_leak_patterns.asp
大家可以到上面这个地址中看到详细的说明,包括范例和图例都有。只是我英文不太好,看不太懂,如果我上述有失误或有需要补充的地方请大家指出。
2、对于第一条,事实上包括 element.onclick = funcRef 这种写法也算在其中,因为这也是一个对对象的引用。在页面onunload时应该释放掉。
3、对于第三条,在MSDN的英文说明中好像是说即使调用detachEvent也无法释放内存,因为在attachEvent的时候就已经造成内存“LEAK”了,不过detachEvent后情况还是会好一点。不知道是不是这样,请英文好的亲能够指出。
4、在实际编程中,这些内存问题的实际影响并不大,尤其是给客户使用时,客户对此绝不会有察觉,然而这些问题对于程序员来说却始终是个心病 --- 有这样的BUG心里总会觉得不舒服吧?能解决则给与解决,这样是最好的。事实上我在webfx.eae.net这样顶级的JS源码站点中,在它们的源码里都会看到采用上述解决方式进行内存的释放管理。
闭包内存泄露分析
该分析不仅仅适用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对象。
以上示例代码执行完成后的对象引用图如下:
图中矩形和椭圆代表对象,箭头代表引用。
闭包的功能非常强大,上面的示例中也不会造成不好的影响。
但是,滥用闭包将十分可怕。
看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 οnlοad='closureTest();'>
</body>
</html>
为了突出主题,我省略了不影响示例的多余标签。
用IE执行一个如上的页面,打开windows任务管理器,查看iexplore.exe进程的“内存使用”。
按几下F5看看内存是如何增加的。^_^
这就是闭包导致内存泄露问题。
看分析:
这是一个闭包应用。为什么?
看这里,看这里,看这里
maskDiv.oncontextmenu = function(){ return false; };
为了说明问题,看下图:
显然,这里出现了循环引用(实箭头是直接引用,虚箭头是间接引用)。为什么?
我们看看程序执行过程:
①. 页面载入完成后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对象,变成了一个全局变量。
变化后的引用关系图如下:
![](http://bbs.51js.com/images/default/attachimg.gif)
很显然,圈地运动不可能发展起来了。但其还是有点不尽如人意。
1. maskDiv是全局变量,当页面复杂时(比如引入了多个js文件),随意污染window对象的全局作用域,将陷入名称空间碰撞的泥潭。
2. 自然情况下,这些对象及引用一旦创建,则只能在页面卸载时被GC回收,有点浪费内存空间。——我们可是有名的铁公鸡。
方法二:战争践踏
在脚本中增加如下代码:
function myGC(){
document.body.removeChild(document.getElementById("myDiv");
}
在body标签上增加onunload事件处理 οnunlοad=” 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自己用了,减少了可能的耦合点,赞!
神说:“简单就是美!”
综上所述,闭包之所以容易引起内存泄露,本质是由于其容易引起循环引用。所以,如何发现并避免出现循环引用是我们要关注的重点。不要被所谓闭包的新名词遮蔽我们的双眼。