JavaScript使用一種稱爲垃圾收集的技術來管理分配給它的內存。這與C這樣的底層語言不一樣,C要求使用多少借多少,用完再釋放回去。其餘語言,好比 Objective-C,實現了一個引用計數系統來輔助完成這些工做,咱們可以瞭解到有多少個程序塊使用了一個特定的內存段,於是能夠在不須要時清除這些內存段。javascript
JavaScript是一種高級語言,它通常是經過後臺來維護這種計數系統。css
當JavaScript代碼生成一個新的內存駐留項時(好比一個對象或函數),系統就會爲這個項留出一塊內存空間。由於這個對象可能會被傳遞給不少函數,而且會被指定給不少變量,因此不少代碼都會指向這個對象的內存空間。JavaScript會跟蹤這些指針,當最後一個指針廢棄不用時,這個對象佔用的內存會被釋放。java
A ---------> B ------------> C
例如對象A有一個屬性指向B,而B也有一個屬性指向C。即便當前做用域中只有對象A有效,但因爲指針的關係全部3個對象都必須保留在內存中。當離開A的當前做用域時(例如代碼執行到聲明A的函數的末尾處),垃圾收集器就能夠釋放A佔用的內存。此時,因爲沒有什麼指向B,所以B能夠釋放,最後,C也能夠釋放。node
然而,當對象間的引用關係變得複雜時,處理起來也會更加困難。web
A ---------> B ------------> C
^、_ _ _ _ _ _ _|
這裏,咱們又爲對象C添加了一個引用B的屬性。在這種狀況下,當A釋放時,仍然有來自C的指針指向B。這種引用循環須要由JavaScript進行特殊的處理,但必須考慮到整個循環與做用域中的其餘變量已經處於隔離狀態。chrome
從這裏咱們能夠看到,閉包問題的本質是做用域的問題,我平時寫的閉包大多出如今:瀏覽器
循環引用
閉包可能會致使在不經意間建立循環引用。由於函數是必須保存在內存中的對象,因此位於函數執行上下文中的全部變量也須要保存在內存中:bash
function outerFn() {
var outerVar = {};
function innerFn() {
console.log(outerVar);
}
outerVar.fn = innerFn;
return innerFn;
};
這裏建立了一個名爲 outerVar 的對象,該對象在內部函數innerFn()中被引用。而後,爲 outerVar 建立了一個指向 innerFn()的屬性,以後返回了innerFn()。這樣就在 innerFn() 上建立了一個引用outerVar的閉包,而outerVar又引用了innerFn()。閉包
這會致使變量在內存中存在的時間比想象得長,並且又不容易被發現。這還不算完,還有可能會出現比這種狀況更隱蔽的引用循環:app
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 源碼的相關說法: