作為一門高級語言,JS并不像低級語言C/C++那樣擁有對內(nèi)存的完全掌控。JS中內(nèi)存的分配和回收都是自動完成的,內(nèi)存在不使用的時候會被垃圾回收器自動回收。
正因?yàn)槔厥掌鞯拇嬖?,許多人認(rèn)為JS不用太關(guān)心內(nèi)存管理的問題,但如果不了解JS的內(nèi)存管理機(jī)制,我們同樣非常容易成內(nèi)存泄漏(內(nèi)存無法被回收)的情況。
內(nèi)存的生命周期
JS環(huán)境中分配的內(nèi)存一般有如下生命周期:
- 內(nèi)存分配:當(dāng)我們申明變量、函數(shù)、對象的時候,系統(tǒng)會自動為他們分配內(nèi)存
- 內(nèi)存使用:即讀寫內(nèi)存,也就是使用變量、函數(shù)等
- 內(nèi)存回收:使用完畢,由垃圾回收自動回收不再使用的內(nèi)存
內(nèi)存分配的幾個例子:
// 為變量分配內(nèi)存
var i = 11;
var s = "ifcode";
// 為對象分配內(nèi)存
var person = {
age: 22,
name: 'ifcode'
};
// 為函數(shù)分配內(nèi)存
function sum(a, b) {
return a + b;
}
垃圾回收算法
對垃圾回收算法來說,核心思想就是如何判斷內(nèi)存已經(jīng)不再使用了。下面介紹兩種常見瀏覽器的垃圾回收算法。
引用計(jì)數(shù)算法
熟悉C語言的同學(xué)的都明白,引用無非就是指向某一物體的指針。對不熟悉底層語言的同學(xué)來說,可簡單將引用視為一個對象訪問另一個對象的路徑。(這里的對象是一個寬泛的概念,泛指JS環(huán)境中的實(shí)體)。
引用計(jì)數(shù)算法定義“內(nèi)存不再使用”的標(biāo)準(zhǔn)很簡單,就是看一個對象是否有指向它的引用。如果沒有其他對象指向它了,說明該對象已經(jīng)不再需了。
下面來看個例子:
// 創(chuàng)建一個對象person,他有兩個指向?qū)傩詀ge和name的引用
var person = {
age: 22,
name: 'ifcode'
};
person.name = null; // 雖然設(shè)置為null,但因?yàn)閜erson對象還有指向name的引用,因此name不會回收
var p = person;
person = 1; //原來的person對象被賦值為1,但因?yàn)橛行乱胮指向原person對象,因此它不會被回收
p = null; //原person對象已經(jīng)沒有引用,很快會被回收
由上面可以看出,引用計(jì)數(shù)算法是個簡單有效的算法。但它卻存在一個致命的問題:循環(huán)引用。如果兩個對象相互引用,盡管他們已不再使用,垃圾回收器不會進(jìn)行回收,導(dǎo)致內(nèi)存泄露。
function cycle() {
var o1 = {};
var o2 = {};
o1.a = o2;
o2.a = o1;
return "Cycle reference!"
}
cycle();
上面我們申明了一個cycle方程,其中包含兩個相互引用的對象。在調(diào)用函數(shù)結(jié)束后,對象o1和o2實(shí)際上已離開函數(shù)范圍,因此不再需要了。但根據(jù)引用計(jì)數(shù)的原則,他們之間的相互引用依然存在,因此這部分內(nèi)存不會被回收,內(nèi)存泄露不可避免了。
正是因?yàn)橛羞@個嚴(yán)重的缺點(diǎn),這個算法在現(xiàn)代瀏覽器中已經(jīng)被下面要介紹的標(biāo)記清除算法所取代了。但絕不可認(rèn)為該問題已經(jīng)不再存在了,因?yàn)檫€占有大量市場的IE6、IE7使用的正是這一算法。在需要照顧兼容性的時候,某些看起來非常普通的寫法也可能造成意想不到的問題:
var div = document.createElement("div");
div.onclick = function() {
console.log("click");
};
上面這種JS寫法再普通不過了,創(chuàng)建一個DOM元素并綁定一個點(diǎn)擊事件。那么這里有什么問題呢?請注意,變量div有事件處理函數(shù)的引用,同時事件處理函數(shù)也有div的引用?。╠iv變量可在函數(shù)內(nèi)被訪問)。一個循序引用出現(xiàn)了,按上面所講的算法,該部分內(nèi)存無可避免地泄露哦了。
現(xiàn)在你明白為啥前端程序員都討厭IE6了吧?擁有超多BUG并依然占有大量市場的IE6是前端開發(fā)一生之?dāng)?!親,沒有買賣就沒有殺害。為了讓你身邊的前端程序員活得健康一些,請今天就升級你的瀏覽器吧!
標(biāo)記清除算法
上面說過,現(xiàn)代的瀏覽器已經(jīng)不再使用引用計(jì)數(shù)算法了。現(xiàn)代瀏覽器通用的大多是基于標(biāo)記清除算法的某些改進(jìn)算法,總體思想都是一致的。
標(biāo)記清除算法將“不再使用的對象”定義為“無法達(dá)到的對象”。簡單來說,就是從根部(在JS中就是全局對象)出發(fā)定時掃描內(nèi)存中的對象。凡是能從根部到達(dá)的對象,都是還需要使用的。那些無法由根部出發(fā)觸及到的對象被標(biāo)記為不再使用,稍后進(jìn)行回收。
從這個概念可以看出,無法觸及的對象包含了沒有引用的對象這個概念(沒有任何引用的對象也是無法觸及的對象)。但反之未必成立。
根據(jù)這個概念,上面的例子可以正確被垃圾回收處理了。當(dāng)div與其時間處理函數(shù)不能再從全局對象出發(fā)觸及的時候,垃圾回收器就會標(biāo)記并回收這兩個對象。
如何寫出對內(nèi)存管理友好的JS代碼?
如果還需要兼容老舊瀏覽器,那么就需要注意代碼中的循環(huán)引用問題?;蛘咧苯硬捎帽WC兼容性的庫來幫助優(yōu)化代碼。
對現(xiàn)代瀏覽器來說,唯一要注意的就是明確切斷需要回收的對象與根部的聯(lián)系。有時候這種聯(lián)系并不明顯,且因?yàn)闃?biāo)記清除算法的強(qiáng)壯性,這個問題較少出現(xiàn)。最常見的內(nèi)存泄露一般都與DOM元素綁定有關(guān):
email.message = document.createElement(“div”);
displayList.appendChild(email.message);
// 稍后從displayList中清除DOM元素
displayList.removeAllChildren();
div元素已經(jīng)從DOM樹中清除,也就是說從DOM樹的根部無法觸及該div元素了。但是請注意,div元素同時也綁定了email對象。所以只要email對象還存在,該div元素將一直保存在內(nèi)存中。
小結(jié)
如果你的引用只包含少量JS交互,那么內(nèi)存管理不會對你造成太多困擾。一旦你開始構(gòu)建中大規(guī)模的SPA或是服務(wù)器和桌面端的應(yīng)用,那么就應(yīng)當(dāng)將內(nèi)存泄露提上日程了。不要滿足于寫出能運(yùn)行的程序,也不要認(rèn)為機(jī)器的升級就能解決一切。當(dāng)你從初級程序員走向資深的時候,關(guān)注細(xì)節(jié)真是你能脫穎而出的優(yōu)點(diǎn)。