JavaScript 是用來向 Web 頁面新增動態內容的一種功能強大的指令碼語言。它尤其特別有助於一些日常任務,比如驗證密碼和建立動態選單元件。JavaScript 易學易用,但卻很容易在某些瀏覽器中引起記憶體的洩漏。在這個介紹性的文章中,我們解釋了 JavaScript 中的洩漏由何引起,展示了常見的記憶體洩漏樣式,並介紹瞭如何應對它們。
註意本文假設您已經非常熟悉使用 JavaScript 和 DOM 元素來開發 Web 應用程式。本文尤其適合使用 JavaScript 進行 Web 應用程式開發的開發人員,也可供有興趣建立 Web 應用程式的客戶提供瀏覽器支援以及負責瀏覽器故障排除的人員參考。
我的瀏覽器存在洩漏麼?
Internet Explorer 和 Mozilla Firefox 是兩個與 JavaScript 中的記憶體洩漏聯絡最為緊密的瀏覽器。兩個瀏覽器中造成這種問題的“罪魁禍首”是用來管理 DOM 物件的元件物件模型。本機 Windows COM 和 Mozilla’s XPCOM 都使用取用計數的垃圾收集來進行記憶體分配和檢索。取用計數與用於 JavaScript 的標記-清除式的垃圾收集並不總是能相互相容。本文側重介紹的是如何應對 JavaScript 程式碼中的記憶體洩漏。有關如何處理 Firefox 和 IE 中 COM 層記憶體洩漏的更多資訊,請參看 參考資料。
JavaScript 中的記憶體洩漏
JavaScript 是一種垃圾收集式語言,這就是說,記憶體是根據物件的建立分配給該物件的,並會在沒有對該物件的取用時由瀏覽器收回。JavaScript 的垃圾收集機制本身並沒有問題,但瀏覽器在為 DOM 物件分配和恢復記憶體的方式上卻有些出入。
Internet Explorer 和 Mozilla Firefox 均使用取用計數來為 DOM 物件處理記憶體。在取用計數系統,每個所取用的物件都會保留一個計數,以獲悉有多少物件正在取用它。如果計數為零,該物件就會被銷毀,其佔用的記憶體也會傳回給堆。雖然這種解決方案總的來說還算有效,但在迴圈取用方面卻存在一些盲點。
迴圈取用的問題何在?
當兩個物件互相取用時,就構成了迴圈取用,其中每個物件的取用計數值都被賦 1。在純垃圾收集系統中,迴圈取用問題不大:若涉及到的兩個物件中的一個物件被任何其他物件取用,那麼這兩個物件都將被垃圾收集。而在取用計數系統,這兩個物件都不能被銷毀,原因是取用計數永遠不能為零。在同時使用了垃圾收集和取用計數的混合系統中,將會發生洩漏,因為系統不能正確識別迴圈取用。在這種情況下,DOM 物件和 JavaScript 物件均不能被銷毀。清單 1 顯示了在 JavaScript 物件和 DOM 物件間存在的一個迴圈取用。
清單 1. 迴圈取用導致了記憶體洩漏
document.write("circular references between JavaScript and DOM!");
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"));
};
Div Element
如上述清單中所示,JavaScript 物件obj擁有到 DOM 物件的取用,表示為DivElement。而 DOM 物件則有到此 JavaScript 物件的取用,由expandoProperty表示。可見,JavaScript 物件和 DOM 物件間就產生了一個迴圈取用。由於 DOM 物件是透過取用計數管理的,所以兩個物件將都不能銷毀。
另一種記憶體洩漏樣式
在清單 2 中,透過呼叫外部函式myFunction建立迴圈取用。同樣,JavaScript 物件和 DOM 物件間的迴圈取用也會導致記憶體洩漏。
清單 2. 由外部函式呼叫引起的記憶體洩漏
正如這兩個程式碼示例所示,迴圈取用很容易建立。在 JavaScript 最為方便的程式設計結構之一:閉包中,迴圈取用尤其突出。
document.write(" object s between JavaScript and DOM!");
function myFunction(element)
{
this.elementReference = element;
// This code forms a circular reference here
//by DOM-->JS-->DOM
element.expandoProperty = this;
}
function Leak() {
//This code will leak
new myFunction(document.getElementById("myDiv"));
}
JavaScript 中的閉包
JavaScript 的過人之處在於它允許函式巢狀。一個巢狀的內部函式可以繼承外部函式的引數和變數,並由該外部函式私有。清單 3 顯示了內部函式的一個示例。
清單 3. 一個內部函式
function parentFunction(paramA) {
var a = paramA;
function childFunction() {
return a + 2;
}
return childFunction();
}
JavaScript 開發人員使用內部函式來在其他函式中整合小型的實用函式。如清單 3 所示,此內部函式childFunction可以訪問外部函式parentFunction的變數。當內部函式獲得和使用其外部函式的變數時,就稱其為一個閉包。
瞭解閉包
考慮如清單 4 所示的程式碼片段。
清單 4. 一個簡單的閉包
document.write("Closure Demo!!");
window.onload=
function closureDemoParentFunction(paramA)
{
var a = paramA;
return function closureDemoInnerFunction (paramB)
{
alert( a +" "+ paramB);
};
};
var x = closureDemoParentFunction("outer x");
x("inner x");
外部函式closureDemoParentFunction的本地變數a即使在外部函式傳回時仍會存在。這一點不同於 C/C++ 這樣的程式語言,在 C/C++ 中,一旦函式傳回,本地變數也將不復存在。在 JavaScript 中,在呼叫closureDemoParentFunction的時候,帶有屬性a的範圍物件將會被建立。該屬性包括值paramA,又稱為“外部 x”。同樣地,當closureDemoParentFunction傳回時,它將會傳回內部函式closureDemoInnerFunction,該函式包括在變數x中。在上述清單中,closureDemoInnerFunction是在父函式closureDemoParentFunction中定義的內部函式。當用外部的 x對closureDemoParentFunction進行呼叫時,外部函式變數a就會被賦值為外部的 x。函式會傳回指向內部函式closureDemoInnerFunction的指標,該指標包括在變數x內。
由於內部函式持有到外部函式的變數的取用,所以這個帶屬性a的範圍物件將不會被垃圾收集。當對具有引數值inner x的x進行呼叫時,即x(“inner x”),將會彈出警告訊息,表明 “outer x innerx”。
清單 4簡要解釋了 JavaScript 閉包。閉包功能非常強大,原因是它們使內部函式在外部函式傳回時也仍然可以保留對此外部函式的變數的訪問。不幸的是,閉包非常易於隱藏 JavaScript 物件 和 DOM 物件間的迴圈取用。
閉包和迴圈取用
在清單 5 中,可以看到一個閉包,在此閉包內,JavaScript 物件(obj)包含到 DOM 物件的取用(透過 id”element”被取用)。而 DOM 元素則擁有到 JavaScriptobj的取用。這樣建立起來的 JavaScript 物件和 DOM 物件間的迴圈取用將會導致記憶體洩漏。
清單 5. 由事件處理引起的記憶體洩漏樣式
document.write("Program to illustrate memory leak via closure");
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"));
// This is used to make the leak significant
};
避免記憶體洩漏
幸好,JavaScript 中的記憶體洩漏是可以避免的。當確定了可導致迴圈取用的樣式之後,正如我們在上述章節中所做的那樣,您就可以開始著手應對這些樣式了。這裡,我們將以上述的由事件處理引起的記憶體洩漏樣式為例來展示三種應對已知記憶體洩漏的方式。
一種應對清單 5中的記憶體洩漏的解決方案是讓此 JavaScript 物件obj為空,這會顯式地打破此迴圈取用,如清單 6 所示。
清單 6. 打破迴圈取用
document.write("Avoiding memory leak via closure by breaking the circular
reference");
window.onload=function outerFunction(){
var obj = document.getElementById("element");
obj.onclick=function innerFunction()
{
alert("Hi! I have avoided the leak");
// Some logic here
};
obj.bigString=new Array(1000).join(new Array(2000).join("XXXXX"));
obj = null; //This breaks the circular reference
};
清單 7 是透過新增另一個閉包來避免 JavaScript 物件和 DOM 物件間的迴圈取用。
清單 7. 新增另一個閉包
document.write("Avoiding a memory leak by adding another closure");
window.onload=function outerFunction(){
var anotherObj = function innerFunction()
{
// Some logic here
alert("Hi! I have avoided the leak");
};
(function anotherInnerFunction(){
var obj = document.getElementById("element");
obj.onclick=anotherObj })();
};
清單 8 則透過新增另一個函式來避免閉包本身,進而阻止了洩漏。
清單 8. 避免閉包自身
document.write("Avoid leaks by avoiding closures!");
window.onload=function()
{
var obj = document.getElementById("element");
obj.onclick = doesNotLeak;
}
function doesNotLeak()
{
//Your Logic here
alert("Hi! I have avoided the leak");
}
結束語
本文解釋了迴圈取用是如何導致 JavaScript 中的記憶體洩漏的 —— 尤其是在結合了閉包的情況下。您還瞭解了涉及到迴圈取用的一些常見記憶體洩漏樣式以及應對這些洩漏樣式的幾種簡單方式。有關本文所討論的主題的更多資訊,請參看參考資料。
參考資料
學習
您可以參閱本文在 developerWorks 全球站點上的英文原文。
“JavaScript and the Document Object Model”(Nicholas Chase,developerWorks,2002 年 7 月):為 JavaScript 開發人員介紹了 DOM。
“跨越邊界:閉包”(Bruce Tate,developerWorks,2007 年 1 月):有關閉包的入門文章(基於 Ruby,但理論上也可以應用到 JavaScript)。
“JavaScript 中的有限狀態機,第 1 部分: 設計一個小部件”(Edward J. Pring,developerWorks,2007 年 1 月):使用閉包和 JavaScript 的其他高階特性的有趣練習。
“A re-introduction to javascript”(Simon Wilson,Mozilla.org):有關 JavaScript 及其特性的深入介紹。
“Using XPCOM in JavaScript without leaking”(David Baron,Mozilla.org):解釋了 Firefox 為何使用取用計數來進行記憶體分配以及它又是如何進行這種分配的。
“對 HTML 頁上 DOM 物件迴圈取用導致記憶體洩漏”(Microsoft 幫助和支援):瞭解 Microsoft 對 IE 中的記憶體洩漏作何解釋。
“Memory leakage in Internet Explorer — revisited”(Volkan Ozcelik,The Code Project,2005 年 11 月):有關 JavaScript 中常見的記憶體洩漏原因(針對 IE)的教程式介紹。
“developerWorks Web 開發專區:內含大量有關 Web 2.0、Ajax、wikis、PHP、mashups 和其他 Web 專案的資源。
原文出處: IBM developerworks