一、内存泄漏


1.所谓内存泄漏,是指浏览器的垃圾回收机制无法正常回收没用的DOM对象,根本原因是DOM对象的引用数大于0。

2.在IE9之前的版本,IE浏览器的DOM对象和JS对象使用了不同的垃圾回收机制。这是内存泄漏的根源。

3.浏览器回收DOM对象,都是查看DOM对象的引用次数是否为0。而有些时候,某个DOM对象的引用次数永远都不会变为0,哪怕是页面已经关闭了。想让内存发生泄漏,做法就是让DOM对象的引用数始终大于0。


二、内存泄漏的种类

1.循环引用(Circular References)


(1)这是一种非常常见的形式。

(2)循环引用,最简单的说法就是定义了DOM对象的某个属性指向其自身。


//小实验
<div id="myDiv"></div>
<script language="javascript">
	var box=document.getElementById("myDiv");
	document.getElementById("box").myProperty=box;
</script>

(3)上面的例子中,解析器维护着两个引用:一个是box,指向了DOM(myDiv);另一个是DOM的myProperty属性,指向了box变量。因此,垃圾回收程序不论从哪个角度看,都不可能释放box和myProperty。


(4)如果这种两个引用的循环很容易看出来,那么换成几十个DOM和Object的集合,循环就不容易看出来了。就算寻找的话,也相当于在有向图里寻找回路,算法复杂度相当高。


2.闭包(Closures)


(1)与循环引用不同,闭包不是通过两个毫不相干的对象构造循环,而是通过引入父函数的局部变量构造循环。

(2)前面提到过,闭包的作用域链中,保留着对父级函数活动对象的引用,开发时,不知不觉就会出现循环。


//小实验
<div id="myDiv"></div>
<script language="javascript">
function bindClick(){
	var element=document.getElementById("myDiv");
	element.addEventListener("click",clickHandler,false);
	function clickHandler(){
		alert(element.id);
	}
}
bindClick();
</script>

(3)从上面例子可以看出,clickHandler是一个闭包。bindClick执行完成后,由于element要继续被clickHandler使用,所以不能将它释放。那为什么点击myDiv后clickHandler执行完毕,element仍然不能释放呢?因为clickHandler是一个处理事件的闭包,而事件并不是只发生一次的,这里暗藏着另一个循环,所以element还是不能销毁。


3.页面交叉泄漏(Cross-Page Leaks) 


(1)在使用js动态创建添加DOM对象时,添加顺序不对,在某种情况下会引起这种泄漏。主要是因为DOM对象创建过程中的临时对象不能即使得到清理造成的。比如下面这个例子。


//小实验
function addDOM(){
	var outer=document.createElement("<div onClick='foo()'>"); 
	var inner=document.createElement("<div onClick='foo()'>"); 
	outer.appendChild(inner);
	document.body.appendChild(outer);
	document.body.removeChild(outer);
	outer.removeChild(inner);
	outer=null;
	inner=null;
}

(2)有一种普遍的说法:我们动态创建两个对象,然后创建一个子元素和父元素间的临时域(这个临时域应该是管理元素之间层次结构关系的对象),当你把这两个元素构成的组合添加到页面中时,这两个元素将会继承页面DOM中的层次管理域对象,并泄漏之前创建的那个临时域对象。


(3)对于这种普遍说法,我理解得不是很透彻,需要找其他资料看一下,尤其是appendChild的具体实现过程。不过大概理解就是,由于outer还没添加到document,那么直接将inner添加到outer时,会产生一个对象描述他们的父子关系;而当outer被添加到document后,父子关系的描述就使用了document自带的另一个对象,然后创建的临时的描述父子关系的对象就泄漏了。


4.貌似泄漏(Pseudo-Leaks)


(1)在大多数时候,一些API的实际的行为和它们预期的行为可能会导致你错误的判断内存泄漏。

(2)这个问题也需要依赖创建临时对象来产生"泄漏"。对一个脚本元素对象内部的脚本文本一而再再而三的反复重写,慢慢地你将开始泄漏各种已关联到被覆盖内容中的脚本引擎对象。


//小实验
<html> 
<head> 
<script language="javascript"> 
function LeakMemory(){ 
	for(i = 0; i < 5000; i++){
		hostElement.text = "function foo(){}"; 
	}
}
</script> 
</head> 
<body οnlοad="LeakMemory()"> 
<script id="hostElement">function foo(){}</script> 
</body> 
</html>

三、内存泄漏的解决


1.循环引用。如果规定了DOM的属性,可以在网页关闭时或者定期执行斩链过程。理论上如果多个对象只存在一个循环,断开一个就可以了。但正如前面说的,这个循环可能会非常复杂,如果是图的话,砍断哪个最有效很难说。因此最简单的办法就是将诸如下面例子里的DOM属性统统砍断。


//小实验
function breaklinks(){
	document.getElementById("box").myProperty = null; 
}

2.闭包。内存泄漏的根本,是DOM的引用数始终大于1。那么在制作闭包时,把需要的DOM对象属性复制到变量中,然后把DOM的引用手动释放掉,这个问题就迎刃而解了。至于什么时候释放局部变量,引擎的内存管理机制做得很到位,完全不用去担心。下面例子中,最后一行手动把element释放掉了,如果不手动释放,element对DOM的引用还是存在的,因为clickHandler的作用域链保留着对bindClick活动对象的引用,而bindClick活动对象里又包含了element和id。我们手动释放后,element仍然是存在的,但element对DOM的引用已经断掉了,所以内存泄漏也就不存在了。这个例子告诉我们,用于事件监听的闭包是很危险的,尽量不要写。


//小实验
<div id="myDiv"></div>
<script language="javascript">
function bindClick(){
	var element=document.getElementById("myDiv");
	var id=element.id;
	element.addEventListener("click",clickHandler,false);
	function clickHandler(){
		alert(id);
	}
	element=null;
}
bindClick();
</script>

3.页面交叉泄漏。如果按照普遍说法,只要调整appendChild的顺序就可以了。比如那个例子创建元素时使用了内联函数,正因为使用了内联函数,才需要创建临时域,毕竟那两个foo()存在着事件冒泡顺序问题,需要一个对象保存他们的顺序。紧接着有了另一种解决思路,就是不使用内联,而是创建添加完成后,再绑定事件。


//解法1
function addDOM(){
	var outer=document.createElement("<div onClick='foo()'>"); 
	var inner=document.createElement("<div onClick='foo()'>"); 
	document.body.appendChild(outer);
	outer.appendChild(inner);
	outer.removeChild(inner);
	document.body.removeChild(outer);
	outer=null;
	inner=null;
}
//解法2
function addDOM(){
	var outer=document.createElement("<div>"); 
	var inner=document.createElement("<div>"); 
	document.body.appendChild(outer);
	outer.appendChild(inner);
	outer.οnclick=foo;
	inner.οnclick=foo;
	outer.removeChild(inner);
	document.body.removeChild(outer);
	outer=null;
	inner=null;
}

4.貌似泄漏。这个基本上属于IE的bug,不用刻意处理,页面切换的时候,泄漏的内存就会回收回来。