首页 » 编写高质量代码:改善JavaScript程序的188个建议 » 编写高质量代码:改善JavaScript程序的188个建议全文在线阅读

《编写高质量代码:改善JavaScript程序的188个建议》建议183:避免内存泄漏

关灯直达底部

JavaScript是一种垃圾收集式语言,其对象的内存是根据对象的创建分配给该对象的,并且会在没有对该对象的引用时由浏览器收回。JavaScript的垃圾收集机制本身并没有问题,但浏览器在为DOM对象分配和恢复内存的方式上有些出入。

IE和Firefox均使用引用计数来为DOM对象处理内存。在引用计数系统中,每个所引用的对象都会保留一个计数,以获悉有多少对象正在引用它。如果计数为零,那么该对象就会被销毁,其占用的内存也会返回给堆。虽然这种解决方案总的来说还算有效,但是在循环引用方面却存在一些盲点。

当两个对象互相引用时,就构成了循环引用,其中每个对象的引用计数值都被赋为1。在纯垃圾收集系统中,循环引用问题不大:如果涉及的两个对象中有一个对象被任何其他对象引用,那么这两个对象都将被垃圾收集。而在引用计数系统中,这两个对象都不能被销毁,原因是引用计数永远不能为零。在同时使用了垃圾收集和引用计数的混合系统中,将会发生泄漏,因为系统不能正确识别循环引用。在这种情况下,DOM对象和JavaScript对象均不能被销毁,例如:


<html>

<body>

<script type=/"text/javascript/">

var obj;

window.onload=function{

obj=document.getElementById(/"DivElement/");

document.getElementById(/"DivElement/").expandoProperty=obj;

obj.bigString=new Array(1000).join(new Array(2000).join(/"XXXXX/"));

};

</script>

<p>Div Element</p>

</body>

</html>


在上面代码中,JavaScript对象obj拥有到DOM对象的引用,表示为DivElement。而DOM对象也拥有到此JavaScript对象的引用,由expandoProperty表示。可见JavaScript对象和DOM对象间就产生了一个循环引用。由于DOM对象是通过引用计数管理的,因此两个对象将都不能销毁。

另一种内存泄漏模式:通过调用外部函数myFunction创建循环引用。同样,JavaScript对象和DOM对象间的循环引用也会导致内存泄漏。


<html>

<head>

<script type=/"text/javascript/">

document.write(/"objects between Javascript and DOM!/");

function myFunction(element){

this.elementReference=element;

element.expandoProperty=this;

}

function Leak{

new myFunction(document.getElementById(/"myDiv/"));

}

</script>

</head>

<body onload=/"Leak/">

<p></p>

</body>

</html>


循环引用很容易创建。在JavaScript最为方便的编程结构之一——闭包中,循环引用尤其突出。JavaScript的优势在于它允许函数嵌套。一个嵌套的内部函数可以继承外部函数的参数和变量,并由该外部函数私有。


<html>

<body>

<script type=/"text/javascript/">

window.onload=function closureDemoParentFunction(paramA){

var a=paramA;

return function closureDemoInnerFunction(paramB){

alert(a+/"/"+paramB);

};

};

var x=closureDemoParentFunction(/"outer x/");

x(/"inner x/");

</script>

</body>

</html>


在上面代码中,closureDemoInnerFunction是在父函数closureDemoParentFunction中定义的内部函数。当用外部的x对closureDemoParentFunction进行调用时,外部函数变量a就会被赋值为外部的x。外部函数会返回指向内部函数closureDemoInnerFunction的指针,该指针包括在变量x内。

外部函数closureDemoParentFunction的本地变量a即使在外部函数返回时仍会存在。这一点与C/C++这样的编程语言不同。在C/C++中,一旦函数返回,本地变量也将不复存在。在JavaScript中,在调用closureDemoParentFunction时,带有属性a的范围对象将会被创建。该属性包括值paramA,又称为“外部x”。同样,当closureDemoParentFunction返回时,它将会返回内部函数closureDemoInnerFunction,该函数包括在变量x中。

由于内部函数持有对外部函数的变量的引用,因此这个带属性a的范围对象将不会被垃圾收集。当对具有参数值inner x的x进行调用时,即执行x(/"inner x/")时,将会弹出警告消息——“outer x innerx”。

JavaScript闭包功能非常强大,原因是它们使内部函数在外部函数返回时也仍然可以保留对此外部函数的变量的访问。不幸的是,闭包非常易于隐藏JavaScript对象和DOM对象间的循环引用。

例如,在下面代码中的闭包内,JavaScript对象(obj)包含到DOM对象的引用(通过id=/"element/"被引用),而DOM元素则拥有到JavaScript obj的引用,这样建立起来的JavaScript对象和DOM对象间的循环引用将会导致内存泄漏。


<html>

<body>

<script type=/"text/javascript/">

window.onload=function outerFunction{

var obj=document.getElementById(/"element/");

obj.onclick=function innerFunction{

alert(/"Hi!I will leak/");

};

obj.bigString=new Array(1000).join(new Array(2000).join(/"XXXXX/"));

};

</script>

<button>Click Me</button>

</body>

</html>


JavaScript内存泄漏是可以避免的。例如,以上述由事件处理引起的内存泄漏模式为例来展示3种应对已知内存泄漏的方式。

❑主动设置JavaScript对象obj为空,显式打破此循环引用。


<script type=/"text/javascript/">

window.onload=function outerFunction{

var obj=document.getElementById(/"element/");

obj.onclick=function innerFunction{

alert(/"Hi!I have avoided the leak/");

};

obj.bigString=new Array(1000).join(new Array(2000).join(/"XXXXX/"));

obj=null;

};

</script>


❑通过添加另一个闭包来避免JavaScript对象和DOM对象间的循环引用。


<script type=/"text/javascript/">

document.write(/"Avoiding a memory leak by adding another closure/");

window.onload=function outerFunction{

var anotherObj=function innerFunction{

alert(/"Hi!I have avoided the leak/");

};(function anotherInnerFunction{

var obj=document.getElementById(/"element/");

obj.onclick=anotherObj

});

};

</script>


❑通过添加另一个函数来避免闭包本身,进而阻止内存泄漏。


<script type=/"text/javascript/">

window.onload=function{

var obj=document.getElementById(/"element/");

obj.onclick=doesNotLeak;

}

function doesNotLeak{

alert(/"Hi!I have avoided the leak/");

}

</script>