前言:這篇文章的主要內容由翻譯而來,原文連接。可是大致內容與原文不盡相同,刪除了一些內容,同時新增部份內容。因爲本文大部份內容是翻譯而來,如有理解不當之處還望諒解並指出,我會盡快進行修改。(心裏:若是有什麼不對的地方還但願你們指出,反正我也不會改 。玩笑話玩笑話 別當真!)javascript
在一些語言中,開發人員須要手動的使用原生語句來顯示的分配和釋放內存。可是在許多高級語言中,這些過程都會被自動的執行。在JavaScript中,變量(對象,字符串,等等)建立的時候爲其分配內存,當再也不被使用的時候會「自動地」釋放這些內存,這個過程被稱爲垃圾回收。這個看似「自動的」釋放資源的本質是一個混亂的來源,給JavaScript(和其餘高等級語言)開發者能夠不去關心內存管理的錯誤印象。這是一個很大的錯誤。java
內存泄漏(Memory Leak)是指程序中己動態分配的堆內存因爲某種緣由程序未釋放或沒法釋放,形成系統內存的浪費,致使程序運行速度減慢甚至系統崩潰等嚴重後果。算法
內存泄漏缺陷具備隱蔽性、積累性的特徵,比其餘內存非法訪問錯誤更難檢測。由於內存泄漏的產生緣由是內存塊未被釋放,屬於遺漏型缺陷而不是過錯型缺陷。此外,內存泄漏一般不會直接產生可觀察的錯誤症狀,而是逐漸積累,下降系統總體性能,極端的狀況下可能使系統崩潰。編程
不管使用哪種編程語言,內存的生命週期幾乎老是如出一轍的 分配內存、使用內存、釋放內存。 在這裏咱們主要討論內存的回收。canvas
這是最簡單的垃圾回收算法。一個對象在沒有被其餘的引用指向的時候就被認爲「可回收的」。 api
對JS引用類型不熟悉的請先百度引用類型,理解了值類型(基本類型)和引用類型以後才能理解下面的代碼瀏覽器
var obj1 = {
obj2: {
x: 1
}
};
//2個對象被建立。 obj2被obj1引用,而且做爲obj1的屬性存在。這裏並無能夠被回收的。
//obj1和obj2都指向了{obj2: {x: 1}}這個對象,這個示例中用`原來的對象`來表示這個對象。
var obj3 = obj1; //obj3也引用了obj1指向的對象。
obj1 = 1; // obj1不引用原來的對象了。此時原來的對象只有obj3在引用。
var obj4 = obj3.obj2; //obj4引用了obj3對象的obj2屬性,
//此時obj2對象有2個引用,一個是做爲obj3的一個屬性,一個是做爲obj4變量。
obj3 = 1;
// 咦,obj1原來對象只有obj3在引用,如今obj3也沒用在引用了。
// obj1原來的對象就淪爲了一隻單身狗,因而乎抓狗大隊就來帶走了它。(好吧、其實內存就能夠被回收了)。
// 然而 obj2對象依然有人愛(被obj4引用)。因此obj2的內存就不會被垃圾回收。
obj4 = null;
// obj2心裏在吶喊:小姐姐不要離開我 QOQ。如今obj2也沒有被引用了,引用計數就是0
也就是能夠被回收了。
複製代碼
簡而言之~,若是內存有人愛,那就不會被回收。若是是單身狗的話,[手動滑稽]。bash
循環引用會形成麻煩session
引用計數在涉及循環引用的時候有一個缺陷。在下面的例子中,建立了2個對象,而且相互引用,這樣建立了一個循環。所以他們其實是無用的,能夠被釋放。然而引用計數算法考慮到2個對象中的每個至少被引用了一次,所以都不能夠被回收。 閉包
function f() {
var o1 = {};
var o2 = {};
o1.p = o2;
o2.p = o1;
}
f();
複製代碼
單身狗內心千萬頭草泥馬在奔騰(我特麼也會本身牽本身手啊,我也會僞裝情侶拍照啊)
別覺得你僞裝不是單身狗就拿你沒辦法了,這個算法肯定了對象是否能夠被達到。 這個算法包含了如下步驟:
window
對象能夠做爲一個'根'在以前的例子中,雖然兩個變量相互引用,但在函數執行完以後,這個兩個變量都沒有被window
對象上的任何對象所引用。所以,他們會被認爲不可到達的。
1:全局變量 JavaScript用一個有趣的方式管理未被聲明的變量:對未聲明的變量的引用在全局對象裏建立一個新的變量。在瀏覽器的狀況下,這個全局對象是window
。換句話說:
function foo(arg) {
bar = 'some text';
}
//等同於
function foo(arg) {
window.bar = 'some text';
}
複製代碼
若是bar
被假定只在foo
函數的做用域裏引用,可是你忘記了使用var
去聲明它,一個意外的全局變量就被聲明瞭。 在這個例子裏,泄漏一個簡單的字符並不會形成很大的傷害,可是它確實有可能變得更糟。 有時有會經過this
來建立意外的全局變量。
爲了防止這些問題發生,能夠在你的JaveScript文件開頭使用
'use strict'
;。這個可使用一種嚴格的模式解析JavaScript來阻止意外的全局變量。
若是有時全局變量被用於暫時儲存大量的數據或者涉及到的信息,那麼在使用完以後應該指定爲null或者從新分配。
2:被遺忘的定時器或者回調 仍是來個栗子吧,定時器可能會產生對再也不須要的DOM節點或者數據的引用。
var serverData = loadData();
setInterval(function() {
var renderer = document.getElementById('renderer');
if(renderer) {
renderer.innerHTML = JSON.stringify(serverData);
}
}, 5000); //每五秒會執行一次
複製代碼
renderer
對象在未來有可能被移除,讓interval
沒有存在的意義。然而當處理器interval
仍然起做用時,renderer
並不能被回收(interval
在對象被移除時須要被中止),若是interval
不能被回收,它的依賴也不可能被回收。這就意味着serverData
,大概保存了大量的數據,也不可能被回收。 現在,大部分的瀏覽器都能並且會在對象變得不可到達的時候回收觀察處理器,甚至監聽器沒有被明確的移除掉。在對象被處理以前,最好也要顯式地刪除這些觀察者。
var element = document.getElementById('launch-button');
var counter = 0;
function onClick(event) {
counter++;
element.innerHtml = 'text ' + counter;
}
element.addEventListener('click', onClick);
// 作一些其餘的事情
element.removeEventListener('click', onClick);
element.parentNode.removeChild(element);
複製代碼
現在,如今的瀏覽器(包括IE和Edge)使用現代的垃圾回收算法,能夠當即發現並處理這些循環引用。換句話說,在一個節點刪除以前也不是必需要調用removeEventListener。 框架和插件例如jQuqery在處理節點(當使用具體的api的時候)以前會移除監聽器。這個是插件內部的處理能夠確保不會產生內存泄漏,甚至運行在有問題的瀏覽器上(哈哈哈 說的就是IE6)。
3: 閉包 閉包是javascript開發的一個關鍵方面,一個內部函數使用了外部(封閉)函數的變量。因爲JavaScript運行的細節,它可能如下面的方式形成內存泄漏:
var theThing = null;
var replaceThing = function () {
var originalThing = theThing;
var unused = function () {
if (originalThing) console.log('hi') //引用了originalThing
};
theThing = {
longStr: new Array(1000000).jojin('*'),
someMethod: function (){
console.log('message');
}
};
};
setInterval(replaceThing,1000);
複製代碼
這些代碼作了一件事情,每次relaceThing
被調用,theThing
得到一個包含大量數據和新的閉包(someMethod
)的對象。同時,變量unused
引用了originalThing
(theThing
是上一次函數被調用時產生的)。已經有點困惑了吧?最重要的事情是一旦爲同一父域中的做用域產生閉包,則該做用域是共享的。
在這個案例中,someMethod
和unused
共享閉包做用域,unused
引用了originalThing
,這阻止了originalThing
的回收,儘管unused
不會被使用,可是someMethod
依然能夠經過theThing
來訪問replaceThing
做用域外的變量(例如某些全局的)。
4:來自DOM的引用 在你要重複的操做DOM節點的時候,存儲DOM節點是十分有用的。可是在你須要移除DOM節點的時候,須要確保移除DOM tree和代碼中儲存的引用。
var element = {
image: document.getElementById('image'),
button: document.getElementById('button')
};
//Do some stuff
document.body.removeChild(document.getElementById('image'));
//這個時候 雖然從dom tree中移除了id爲image的節點,可是還保留了一個對該節點的引用。因而image仍然不能被回收。
複製代碼
當涉及到DOM樹內部或子節點時,須要考慮額外的考慮因素。例如,你在JavaScript中保持對某個表格的特定單元格的引用。有一天你決定從DOM中移除表格可是保留了對單元格的引用。你也許會認爲除了單元格其餘的都會被回收。實際並非這樣的:單元格是表格的一個子節點,子節點保持了對父節點的引用。確切的說,JS代碼中對單元格的引用形成了整個表格被留在內存中了,因此在移除有被引用的節點時候要移除其子節點。