摘要:該文主要對Web前端開發中經常使用的瀏覽器端腳本語言JavaScript進行分析,討論了其中閉包技術的使用,并結合IE瀏覽器中內存占用情況分析了不當使用閉包技術造成的內存泄漏情況及避免方法。
關鍵詞:JavaScript;閉包;IE瀏覽器;內存泄漏
中圖分類號:TP393文獻標識碼:A文章編號:1009-3044(2008)35-2134-03
Closure in JavaScript Analyze of Memory Leak in IE
ZHANG Yun-fan
(Tongji University, Shanghai 201804, China)
Abstract: This essay mainly researched the Web browser script JavaScript which is widely used in Web development and analyzed the usage of Closure technique. It also researched memory leak and its settlement in Internet Explorer according to memory occupancy rate caused by improper usage of Closure.
Key words: javascript; closure; internet explorer; memory leak
1 JavaScript及DOM文檔模型概述
1.1 JavaScript概述
JavaScript是NetSpace公司引入Java概念所創造的一門語言。最初創建時是為了動態的修改頁面標記,但隨著文檔對象模型(Document object Model,DOM)的出現,以及國際互聯網聯盟(W3C)完成對DOM的標準化,歐洲計算機制造協會(ECMA)也將JavaScript納入ECMAScript規約。
JavaScript是一種解釋型語言,它以程序段的形式包含在html代碼中,當用戶發送請求時,服務器不會對JavaScript進行編譯而是直接將其發送到客戶端,由瀏覽器解釋,因此JavaScript面臨著面對不同瀏覽器的兼容性問題。
JavaScript是一種基于對象的語言而不是面向對象的,由于它支持一定的面向對象技術,所以很多組織和個人利用此開發出了很多框架和工具,使得JavaScript更加具有面向對象的能力。其中閉包就是在實現這些功能時必須要用到的重要特性。
1.2 文檔對象模型(DOM)概述
1.2.1 DOM模型
文檔對象模型(Document object Model,DOM)是提供對文檔內容、結構、風格進行訪問和更新的應用程序接口(API),它是由W3C制定的標準,是一個能讓程序和腳本動態訪問和更新文檔的語言平臺。DOM可以分為3個部分:核心、XML和HTML。DOM和 JavaScript沒有直接關系,其他語言也可以使用DOM。但是由于DOM的出現,JavaScript成為了一種非常強有力的頁面工具。HTML DOM將整個html文檔表述成頁面里的一個樹型結構,其根節點是Document對象,每個元素和屬性都是樹上的一個節點。比如如下一段簡單的html代碼,將其表示成HTML DOM的樹型結構就如圖1所示。
<html>
<head>
<title>你好</title>
</head>
<body>
<h1>標題</h1>
<p>段落</p>
</body>
</html>
1.2.2 使用JavaScript操作HTML DOM
JavaScript可以遍歷DOM文檔樹上的任何節點,動態添加或者刪除節點,也可以對節點的屬性進行修改或對該節點存值進行修改。JavaScript的操作會立即作用的DOM樹上。DOM的部分常見對象如下:
1) Document對象:
Document 對象代表整個 HTML 文檔,可用來訪問頁面中的所有元素。Document 對象是 Window 對象的一個部分,可通過 window.document 屬性來訪問。
2) Event對象:
Event 對象代表事件的狀態,比如事件在其中發生的元素、鍵盤按鍵的狀態、鼠標的位置、鼠標按鈕的狀態。事件通常與函數結合使用,函數不會在事件發生前被執行。
3) Windows對象
Window 對象是 JavaScript 層級中的頂層對象。Window 對象代表一個瀏覽器窗口或一個框架。Window 對象會在 <body> 或 <frameset> 每次出現時被自動創建。
2 JavaScript中的閉包
2.1 閉包的概念
2.1.1 ECMAJavaScript的垃圾收集
ECMAJavaScript 要求使用自動垃圾收集機制。但是規范沒用規定如何實現,所以不同的瀏覽器實現方式不一樣。一般的思想就是模仿JAVA語言,即如果對象不再“可引用(由于不存在對它的引用,使執行代碼無法再訪問到它)”時,該對象就成為垃圾收集的目標。因而,在將來的某個時刻會將這個對象銷毀并將它所占用的一切資源釋放,以便操作系統重新利用。同時,若只有兩個相互引用的對象,他們不會被第三者引用的話,這兩個對象也會被回收。反言之,如果某對象被其他對象引用,引用者又被引用,只要該引用鏈不形成循環,那么只要最初的引用調用者的生命周期不結束,那么整條鏈上的變量的生命周期都不會結束。
正常情況下,一個函數的生命周期結束時就會滿足垃圾收集條件。此時,作用域中的對象,都不再“可引用”,因此將成為垃圾收集的目標。 然而由于不同瀏覽器的實現不同,在IE瀏覽器中對垃圾收集出現了一些問題,如果使用閉包不當,在一些特殊情況下,將會引起內存的泄漏。
2.1.2 閉包的構造
所謂“閉包”,指的是一個擁有許多變量和綁定了這些變量的環境的表達式(通常是一個函數),因而這些變量也是該表達式的一部分。
這段話是官方的定義,但是卻比較艱澀。下面通過一個簡單的例子可以說明如何構建一個閉包:
<script type=\"text/javascript\">
function test(num) {
var sum =100;
/* function Alert(){alert(sum);} */
var Alert = function() { sum = sum + num; alert(sum); }
return Alert;
}
var fun =test(6);
fun();
</script>
這個程序的運行結果是106。
可以看到在函數test里創建了一個內部的匿名函數函數,并用一個變量Alert指向了該匿名函數。該匿名函數也可按注釋中的構建,效果是同樣的。這樣可以看的比較清晰,一個閉包的構成是通過在對一個函數調用的執行環境中返回一個函數對象構成的。比如,在對函數調用的過程中,將一個對內部函數對象的引用指定給另一個對象的屬性。或者,直接將這樣一個(內部)函數對象的引用指定給一個全局變量、或者一個全局性對象的屬性,或者一個作為參數以引用方式傳遞給外部函數的對象。結合垃圾收集機制來看,一個閉包就是當一個函數返回時,一個沒有釋放資源的棧區,更加通俗的講就是方法內部變量的生命周期超過了方法本身的生命周期。
2.2 閉包的應用
從前面的例子可以看出,閉包主要有2個方面的作用:
第一,在內存里保存一個變量。如上例的變量sum和num,由于fun是test函數內部函數Alert的引用,所以根據垃圾回收原則,Alert函數將不被回收,它所引用的sum和num變量也不被回收,他們將一直存在于內存中。
第二,保護函數內部變量。如上例中的sum變量,它只能被Alert函數訪問,提高了安全性。
下面舉例說明閉包的實際應用。
一個常見的例子是使用setTimout函數來延時執行某些函數。setTimout函數的第一個參數是要執行的函數,第二個參數是間隔時間。可以傳遞給第一個參數一個函數的引用,但是無法同時傳遞該函數的參數。這個問題可以用閉包來解決。例子的代碼如下:
function call (A, B, C){
return (function(){
//以下是對參數的一些操作
A=B+C;
});
}
...
var fun= call (1, 2, 3);
main=setTimeout(fun, 500);
call函數構造了一個閉包,fun是對call函數內部匿名函數的引用,根據垃圾回收機制,call函數內的該匿名函數和它所引用的變量A,B,C都不會被回收,也就是說參數A,B,C的值都一直存在于內存里。后面setTimeout函數使用引用fun的時候,前面fun所指向函數的外部函數call的參數值就得以保存。
閉包機制還可以用來封裝功能。當一個函數要使用其他函數,而其他函數并不會被其他代碼直接調用,那么可以使用閉包技術將其他函數放到函數內部封裝起來,對外使用公共的接口,這樣可以提高代碼的可移植性。
3 不當使用閉包帶來的內存泄漏問題及解決方案
3.1 Internet Explorer 的內存泄漏問題
Internet Explorer 瀏覽器的垃圾收集系統中存在一個問題,即如果 ECMAJavaScript 和某些宿主對象構成了 \"循環引用\",那么這些對象將不會被當作垃圾收集。此時所謂的宿主對象指的是任何 DOM 節點和 ActiveX 對象。循環引用是指當兩個或多個對象以首尾相連的方式相互引用。按照垃圾回收機制,循環引用的對象應當被收集處理。但是,在 Internet Explorer 中,如果循環引用中的任何對象是 DOM 節點或者 ActiveX 對象,垃圾收集系統則不會發現它們與外界對象是隔離的。它們就一直保存在內存里,直至瀏覽器關閉。
3.2 閉包函數引起的循環引用及解決方案
從前面可以知道循環引用是造成內存泄漏的原因,而閉包的使用十分容易造成循環引用,因此可以說閉包的使用是web開發中造成內存泄漏的重要原因之一。IE瀏覽器的COM組件產生的對象實例和網頁腳本引擎產生的對象實例相互引用,就會造成內存泄漏。見圖2。
圖3.1所示,腳本對象和DOM對象形成了循環引用。下面的一段代碼展示了web開發中如何通過閉包造成內存泄漏。
<html>
<script language=\"JavaScript\">
function closureTest (){
var TestDiv = document.createElement(\"div\");
TestDiv.id = \"LeakedDiv\";
TestDiv.onclick = function(){TestDiv.style.backgroundColor = \"red\"; };
document.body.appendChild(TestDiv);
}
</script>
<body onload=\"closureTest()\">
</body>
</html>
可以看到closureTest方法里創建了一個內部匿名函數,由于創建的div塊的一個onclick屬性引用了該匿名函數,該div塊是一個外部對象,所以該匿名函數得以在內存里保留,它所要使用的TestDiv對象也就一直存于內存中,形成了內存泄漏。
如何解決這個問題呢,一個比較好的辦法是在body標簽里添加onunload事件的處理函數,讓它來將形成循環的div塊的屬性置空。代碼如下:
<html>
<script language=\"JavaScript\">
function closureTest (){
//代碼略
}
function BreakLeak() {
document.getElementById(\"LeakedDiv\").onclick = 1;
}
</script>
<body onload=\"closureTest()\" onunload=\"BreakLeak()\">
</body>
</html>
這樣,onclick屬性就不會再引用匿名函數,循環引用也就中斷,TestDiv對象將會最終被進行垃圾回收。
4 總結
Web開發使用廣泛,其中尤以JavaScript為最常用的瀏覽器端腳本語言。許多程序員使用大量JavaScript高級技巧或者使用流行的庫,如JQuery,Prototpye,Dojo等。這些應用大多涉及到閉包技術,使用閉包技術能都讓JavaScript實現許多強大的功能。但如果不當使用閉包技術,將會加重瀏覽器負擔破,壞用戶體驗。因此提出對閉包技術弊端的解決方案,將有助于改善Web應用性能。
參考文獻:
[1] 周愛民.JAVASCRIPT語言精髓與編程實踐[M].北京:電子工業出版社,2008.
[2] DOM.http://www.w3school.com.cn/htmldom/index.asp[EB/OL].
[3] Michael Moncur.Sams Teach Yourself JavaScript in 24 Hours[M].4th ed.Sams publishing,2007.