JavaScript的閉包和內存泄漏問題


閉包

http://www.ruanyifeng.com/blog/2009/08/learning_javascript_closures.html

JavaScript中必須提到的功能最強大的抽象概念之一:閉包。它究竟是做什么的呢?

1 function makeAdder(a) {
2     return function(b) {
3         return a + b;
4     }
5 }
6 var x = makeAdder(5);
7 var y = makeAdder(20);
8 x(6); // 11
9 y(7); // 27

makeAdder這個名字本身應該能說明函數是用來做什么的:他創建了一個新的adder函數,這個函數自身帶有一個參數,它被調用的時候這個參數會被加載在外層函數傳進來的參數上。

這里發生的事情和前面介紹過的內嵌函數十分相似:一個函數被定義在了另外一個函數的內部,內部函數可以訪問外部函數的變量。唯一的不同是,外部函數被返回了,那么常識告訴我們局部變量“應該”不再存在。但是它們卻仍然存在——否則 adder 函數將不能工作。也就是說,這里存在makeAdder 的局部變量的兩個不同的“副本”——一個是 a 等於5,另一個是 a 等於20。運行結果就是一個是11一個是27。

下面來說說到底發生了什么。當JavaScript執行一個函數時,都會創建一個作用域對象(scope object),用來保存在這個函數中創建的局部變量。他和被傳入函數的變量一起被初始化。這與那些保存的所有全部變量和函數的全局變量(global object)類似,但仍有很重要的區別,第一,每次函數被執行的時候,就會創建一個新的,特定的作用域對象;第二,與全局對象(在瀏覽器里面是當做window對象來訪問的)不同的是,你不能從JavaScript代碼中直接訪問作用與對象,也沒有可以遍歷當前作用與對象里面屬性的方法。

所以當調用 makeAdder 時,解釋器創建了一個作用域對象,它帶有一個屬性:a,這個屬性被當作參數傳入 makeAdder 函數。然后 makeAdder 返回一個新創建的函數。通常 JavaScript 的垃圾回收器會在這時回收 makeAdder 創建的作用域對象,但是返回的函數卻保留一個指向那個作用域對象的引用。結果是這個作用域對象不會被垃圾回收器回收,直到指向 makeAdder 返回的那個函數對象的引用計數為零

作用域對象組成了一個名為作用域鏈(scope chain)的鏈。它類似於原形(prototype)鏈一樣,被 JavaScript 的對象系統使用。

一個閉包就是一個函數和被創建的函數中的作用域對象的組合。

閉包允許你保存狀態——所以它們通常可以代替對象來使用。

內存泄漏 

使用閉包的一個壞處是,在 IE 瀏覽器中它會很容易導致內存泄露。JavaScript是一種具有垃圾回收機制的語言——對象在創建時分配內存,然后當值想這個對象的引用計數為零時,瀏覽器會回收內存。宿主環境提供的對象都是按照這種方法被處理的。

瀏覽器主機需要處理大量的對象來描繪一個正在被展現的HTML頁面——DOM對象。瀏覽器負責管理他們的內存分配和回收。

IE瀏覽器有自己一套垃圾回收機制,這套機制與JavaScript提供的垃圾回收機制交互時,可能發生內存泄漏。

在IE中,每當在一個JavaScript對象和一個本地對象之間形成循環引用時就會發生內存泄漏。如下所示:

1 function leakMemory() {
2     var el = document.getElementById('el');
3     var o = { 'el': el };
4     el.o = o;
5 }

這段代碼的循環引用會導致內存泄露:IE 不會釋放被 el 和 o 使用的內存,直到瀏覽器被徹底關閉並重啟后。

這個例子往往無法引起人們的重視:一般只會在長時間運行的應用程序中,或者因為巨大的數據量和循環中導致內存泄露發生時,內存泄露才會引起注意。

不過一般也很少發生如此明顯的內存泄露現象——通常泄露的數據結構有多層的引用(references),往往掩蓋了循環引用的情況。

閉包很容易發生無意識的內存泄露。如下所示:

1 function addHandler() {
2     var el = document.getElementById('el');
3     el.onclick = function() {
4         el.style.backgroundColor = 'red';
5     }
6 }

這段代碼創建了一個元素,當它被點擊的時候變紅,但同時它也會發生內存泄露。為什么?因為對el 的引用不小心被放在一個匿名內部函數中。這就在 JavaScript 對象(這個內部函數)和本地對象之間(el)創建了一個循環引用

這個問題有很多種解決方法,最簡單的一種是不要使用 el 變量:

1 function addHandler(){
2     document.getElementById('el').onclick = function(){
3         this.style.backgroundColor = 'red';
4     };
5 }

有趣的是,有一種竅門解決因閉包而引入的循環引用,是添加另外一個閉包:

1 function addHandler() {
2     var clickHandler = function() {
3         this.style.backgroundColor = 'red';
4     };
5     (function() {
6         var el = document.getElementById('el');
7         el.onclick = clickHandler;
8     })();
9 }

內部函數被直接執行,並在 clickHandler 創建的閉包中隱藏了它的內容。

另外一種避免閉包的好方法是在 window.onunload 事件發生期間破壞循環引用。很多事件庫都能完成這項工作。注意這樣做將使 Firefox 中的 bfcache 無法工作。所以除非有其他必要的原因,最好不要在 Firefox 中注冊一個unload 的監聽器。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM