讀古今文學網 > 編寫高質量代碼:改善JavaScript程序的188個建議 > 建議183:避免內存洩漏 >

建議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>