JavaScript 是用來向 Web 頁面添加動態內容的一種功能強大的腳本語言。它尤其特別有助于一些日常任務,比如驗證密碼和創建動態菜單組件。JavaScript 易學易用,但卻很容易在某些瀏覽器中引起內存的泄漏。在這個介紹性的文章中,我們解釋了JavaScript 中的泄漏由何引起,展示了常見的內存泄漏模式,并介紹了如何應對它們。
注意本文假設您已經非常熟悉使用 JavaScript 和 DOM 元素來開發 Web 應用程序。本文尤其適合使用 JavaScript 進行 Web 應用程序開發的開發人員,也可供有興趣創建 Web 應用程序的客戶提供瀏覽器支持以及負責瀏覽器故障排除的人員參考。
JavaScript 是一種垃圾收集式語言,這就是說,內存是根據對象的創建分配給該對象的,并會在沒有對該對象的引用時由瀏覽器收回。JavaScript 的垃圾收集機制本身并沒有問題,但瀏覽器在為 DOM 對象分配和恢復內存的方式上卻有些出入。
Internet Explorer 和 Mozilla Firefox 均使用引用計數來為 DOM 對象處理內存。在引用計數系統,每個所引用的對象都會保留一個計數,以獲悉有多少對象正在引用它。如果計數為零,該對象就會被銷毀,其占用的內存也會返回給堆。雖然這種解決方案總的來說還算有效,但在循環引用方面卻存在一些盲點。
當兩個對象互相引用時,就構成了循環引用,其中每個對象的引用計數值都被賦 1。在純垃圾收集系統中,循環引用問題不大:若涉及到的兩個對象中的一個對象被任何其他對象引用,那么這兩個對象都將被垃圾收集。而在引用計數系統,這兩個對象都不能被銷毀,原因是引用計數永遠不能為零。在同時使用了垃圾收集和引用計數的混合系統中,將會發生泄漏,因為系統不能正確識別循環引用。在這種情況下,DOM 對象和 JavaScript 對象均不能被銷毀。清單 1 顯示了在 JavaScript 對象和 DOM 對象間存在的一個循環引用。
清單 1. 循環引用導致了內存泄漏
<html> <body> <script type="text/javascript"> 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")); }; </script> <div id="DivElement">Div Element</div> </body> </html> |
如上述清單中所示,JavaScript 對象 obj
擁有到 DOM 對象的引用,表示為DivElement
。而 DOM 對象則有到此 JavaScript 對象的引用,由 expandoProperty
表示。可見,JavaScript 對象和 DOM 對象間就產生了一個循環引用。由于 DOM 對象是通過引用計數管理的,所以兩個對象將都不能銷毀。
在清單 2 中,通過調用外部函數 myFunction
創建循環引用。同樣,JavaScript 對象和 DOM 對象間的循環引用也會導致內存泄漏。
清單 2. 由外部函數調用引起的內存泄漏
<html> <head> <script type="text/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")); } </script> </head> <body onload="Leak()"> <div id="myDiv"></div> </body> </html> |
正如這兩個代碼示例所示,循環引用很容易創建。在 JavaScript 最為方便的編程結構之一:閉包中,循環引用尤其突出。
JavaScript 的過人之處在于它允許函數嵌套。一個嵌套的內部函數可以繼承外部函數的參數和變量,并由該外部函數私有。清單 3 顯示了內部函數的一個示例。
清單 3. 一個內部函數
function parentFunction(paramA) { var a = paramA; function childFunction() { return a + 2; } return childFunction(); } |
JavaScript 開發人員使用內部函數來在其他函數中集成小型的實用函數。如清單 3 所示,此內部函數 childFunction
可以訪問外部函數 parentFunction
的變量。當內部函數獲得和使用其外部函數的變量時,就稱其為一個閉包。
考慮如清單 4 所示的代碼片段。
清單 4. 一個簡單的閉包
<html> <body> <script type="text/javascript"> 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"); </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”。
清單 4 簡要解釋了 JavaScript 閉包。閉包功能非常強大,原因是它們使內部函數在外部函數返回時也仍然可以保留對此外部函數的變量的訪問。不幸的是,閉包非常易于隱藏 JavaScript 對象 和 DOM 對象間的循環引用。
在清單 5 中,可以看到一個閉包,在此閉包內,JavaScript 對象(obj
)包含到 DOM 對象的引用(通過 id "element"
被引用)。而 DOM 元素則擁有到 JavaScript obj
的引用。這樣建立起來的 JavaScript 對象和 DOM 對象間的循環引用將會導致內存泄漏。
清單 5. 由事件處理引起的內存泄漏模式
<html> <body> <script type="text/javascript"> 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 }; </script> <button id="element">Click Me</button> </body> </html> |
幸好,JavaScript 中的內存泄漏是可以避免的。當確定了可導致循環引用的模式之后,正如我們在上述章節中所做的那樣,您就可以開始著手應對這些模式了。這里,我們將以上述的 由事件處理引起的內存泄漏模式 為例來展示三種應對已知內存泄漏的方式。
一種應對 清單5 中的內存泄漏的解決方案是讓此 JavaScript 對象 obj
為空,這會顯式地打破此循環引用,如清單 6 所示。
清單 6. 打破循環引用
<html> <body> <script type="text/javascript"> 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 }; </script> <button id="element">"Click Here"</button> </body> </html> |
清單 7 是通過添加另一個閉包來避免 JavaScript 對象和 DOM 對象間的循環引用。
清單 7. 添加另一個閉包
<html> <body> <script type="text/javascript"> 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 })(); }; </script> <button id="element">"Click Here"</button> </body> </html> |
清單 8 則通過添加另一個函數來避免閉包本身,進而阻止了泄漏。
清單 8. 避免閉包自身
<html> <head> <script type="text/javascript"> 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"); } </script> </head> <body> <button id="element">"Click Here"</button> </body> </html> |
本文解釋了循環引用是如何導致 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 產品的評估版:實際體驗來自 DB2?、Lotus?、Rational?、Tivoli? 和 WebSphere? 的應用程序開發工具和中間件產品。
討論

Abhijeet Bhattacharya 是 IBM 印度軟件實驗室的一名系統工程師。在過去三年中,他一直是 OS/2 IBM Web Browser 支持團隊中的一員。他也具有系統管理領域的相關經驗,并參與過 IBM Pegasus 開源創新項目。他目前工作的重點包括分布式計算和 SARPC。他擁有 Rajiv Gandhi Technical University 的工程學士學位。

Kiran Shivarama Sundar 是 IBM 印度軟件實驗室的一名系統工程師。在過去三年中,他一直是 OS/2 IBM Web Browser 支持團隊中的一員。他同時也具有諸多其他項目的工作經驗,包括為 Apache Tuscany Open Source Project 開發命令行工具以及為 IBM 的 EPCIS 團隊開發 RFIDIC Installer。目前,Kiran 加入了 IBM WebSphere Adapters 支持團隊,負責提供對 JMS 和 MQ 適配器的支持。他已成功獲得了 Sun Certified Java Programmer、Sun Certified Web Component Developer 和 Sun Certified Business Component Developer 的認證。他目前所關注的領域包括 Java、J2EE、Web 服務和 SOA。他擁有 Visweshwaraya Technology University 的工程學士學位。