JavaScript使用一種稱為垃圾收集的技術來管理分配給它的內存。這與C這樣的底層語言不同,C要求使用多少借多少,用完再釋放回去。其他語言,比如 Objective-C,實現了一個引用計數系統來輔助完成這些工作,我們能夠了解到有多少個程序塊使用了一個特定的內存段,因而可以在不需要時清除這些內存段。
JavaScript是一種高級語言,它一般是通過后台來維護這種計數系統。
當JavaScript代碼生成一個新的內存駐留項時(比如一個對象或函數),系統就會為這個項留出一塊內存空間。因為這個對象可能會被傳遞給很多函數,並且會被指定給很多變量,所以很多代碼都會指向這個對象的內存空間。JavaScript會跟蹤這些指針,當最后一個指針廢棄不用時,這個對象占用的內存會被釋放。
A ---------> B ------------> C
例如對象A有一個屬性指向B,而B也有一個屬性指向C。即使當前作用域中只有對象A有效,但由於指針的關系所有3個對象都必須保留在內存中。當離開A的當前作用域時(例如代碼執行到聲明A的函數的末尾處),垃圾收集器就可以釋放A占用的內存。此時,由於沒有什么指向B,因此B可以釋放,最后,C也可以釋放。
然而,當對象間的引用關系變得復雜時,處理起來也會更加困難。
A ---------> B ------------> C
^、_ _ _ _ _ _ _|
這里,我們又為對象C添加了一個引用B的屬性。在這種情況下,當A釋放時,仍然有來自C的指針指向B。這種引用循環需要由JavaScript進行特殊的處理,但必須考慮到整個循環與作用域中的其他變量已經處於隔離狀態。
從這里我們可以看到,閉包問題的本質是作用域的問題,我平時寫的閉包大多出現在:
循環引用
閉包可能會導致在不經意間創建循環引用。因為函數是必須保存在內存中的對象,所以位於函數執行上下文中的所有變量也需要保存在內存中:
function outerFn() {
var outerVar = {};
function innerFn() {
console.log(outerVar);
}
outerVar.fn = innerFn;
return innerFn;
};
這里創建了一個名為 outerVar 的對象,該對象在內部函數innerFn()中被引用。然后,為 outerVar 創建了一個指向 innerFn()的屬性,之后返回了innerFn()。這樣就在 innerFn() 上創建了一個引用outerVar的閉包,而outerVar又引用了innerFn()。
這會導致變量在內存中存在的時間比想象得長,而且又不容易被發現。這還不算完,還有可能會出現比這種情況更隱蔽的引用循環:
function outerFn() {
var outerVar = {};
function innerFn() {
console.log('hello');
}
outerVar.fn = innerFn;
return innerFn;
};
這里我們修改了innerFn(),不再招惹 outerVar。但是,這樣做仍然沒有斷開循環引用。
即使innerFn()不再勾引 outerVar,outerVar 也仍然位於innerFn()的封閉環境中。由於閉包的原因,位於 outerFn()中的所有變量都隱含地被 innerFn()所引用。我們再想一想,在 java 中的內部類不也是類似當前情況嗎,內部類能夠‘看’外部的 this。此時此刻,正如彼時彼刻,竟如此相像。因此,閉包會使意外地創建這些引用循環變得易如反掌。
DOM與JavaScript的循環
雖然我很早就知道閉包,也在調試內存問題時在 chrome F12 里的 profile 是里看到 closure reference
,但是並不清除這個問題的根源。因為上述情況通常不是什么問題,JavaScript能夠檢測到這些情況並在它們孤立時將其清除。
最近看到關於這個問題的解釋:在舊版本IE中存在一種難以處理的循環引用問題。
當一個循環中同時包含DOM元素和常規JavaScript對象時,IE無法釋放任何一個對象——因為這兩類對象是由不同的內存管理程序負責管理的。
除非關閉瀏覽器,否則這種循環在IE中永遠得不到釋放。為此,隨着時間的推移,這可能會導致大量內存被無效地占用。
導致這種循環的一個常見原因是簡單的事件處理:
$(document).ready(function() {
var button = document.getElementById('button-1');
button.onclick = function() {
console.log('hello');
return false;
};
});
當指定單擊事件處理程序時,就創建了一個在其封閉的環境中包含button變量的閉包。而且,現在的button也包含一個指向閉包(onclick屬性自身)的引用。這樣,就導致了在IE中即使離開當前頁面也不會釋放這個循環。
為了釋放內存,就需要斷開循環引用,例如關閉窗口,刪除onclick屬性。另外,也可以像下面這樣重寫代碼來
避免這種閉包:
function hello() {
console.log('hello');
return false;
}
$(document).ready(function() {
var button = document.getElementById('button-1');
button.onclick = hello;
});
因為hello()函數不再包含 button,引用就成了單向的(從button到hello),不存的循環,所以就不會造成內存泄漏了。
用jQuery化解引用循環
下面,我們通過常規的jQuery結構來編寫同樣的代碼:
$(document).ready(function() {
var $button = $('#button-1');
$button.click(function(event) {
event.preventDefault();
console.log('hello');
});
});
即使此時仍然會創建一個閉包,並且也會導致同前面一樣的循環,但這里的代碼卻不會使 IE
發生內存泄漏。由於jQuery考慮到了內存泄漏的潛在危害,所以它會手動釋放自己指定的所有事件處理程序。只要堅持使用jQuery的事件綁定方法,就無需為這種特定的常見原因導致的內存泄
漏而擔心。
但是,這並不意味着我們完全脫離了險境。當對DOM元素進行其他操作時,仍然要處處留心。只要是將JavaScript對象指定給DOM元素,就可能在舊版本IE中導致內存泄漏。jQuery只是有助於減少發生這種情況的可能性。
有鑒於此,jQuery為我們提供了另一個避免這種泄漏的工具。用.data()方法,將信息附加到DOM元素。由於這里的數據並非直接保存在擴展屬性中(jQuery使用一個內部對象並通過它創建的ID來保存這里所說的數
據),因此永遠也不會構成引用循環,從而有效回避了內存泄漏問題。
下面附上 jQuery 源碼的相關說法:
// We have to handle DOM nodes and JS objects differently because IE6-7
// can't GC object references properly across the DOM-JS boundary
// Only DOM nodes need the global jQuery cache; JS object data is
// attached directly to the object so GC can occur automatically