JavaScript是一門非常靈活的動態(tài)語言,和Java一樣从媚,JavaScript也具有動態(tài)內(nèi)存回收機制(垃圾回收)逞泄。也就是說,如果一個對象沒有任何人引用了拜效,內(nèi)存就會被自動釋放喷众,不需要像C語言那樣手動調(diào)用free()方法。
所以紧憾,避免JavaScript內(nèi)存泄漏的唯一黃金法則就是:
不要引用不再需要的變量
但現(xiàn)實情況是到千,你一不小心就內(nèi)存泄漏了。如果程序運行變慢赴穗,或者瀏覽器直接崩潰父阻,那肯定是有內(nèi)存泄漏問題了愈涩。內(nèi)存泄漏的罪魁禍首無外乎下面三種情況(代碼見GitHub):
- 全局變量
- 全局監(jiān)聽
- 定時器
下面我們就分別用實例介紹這三種內(nèi)存泄漏情況,并給出了修復內(nèi)存泄漏的方法加矛,以及用Chrome開發(fā)者工具查找內(nèi)存泄漏的方法履婉。
全局變量
全局變量是所有編程語言中最慎用的功能,JavaScript中有兩種聲明全局變量的方式:
- 在任何函數(shù)外部聲明變量
<script>
var leakObject = new LeakObject();
</script>
- 給window添加屬性
function addLeak () {
window.leakObject = new LeakObject();
}
除了這兩種主動聲明全局變量的方式外斟览,還有下面這兩種非常隱蔽的方式也會聲明全局變量毁腿,也就是最常見很隱蔽的內(nèi)存泄漏的方式:
- 不聲明變量,直接給變量賦值(相當于給window增加了屬性)
function addLeak () {
leakObject = new LeakObject();
}
- 函數(shù)體內(nèi)給this賦值屬性苛茂,但調(diào)用函數(shù)時不用對象去調(diào)用已烤,而是直接調(diào)用函數(shù)(這時候this是window)
function addLeak () {
this.leakObject = new LeakObject();
}
addLeak()
不過上面這2種情況可以通過設(shè)置“嚴格模式”避免,關(guān)于“嚴格模式”妓羊,可以參考MDN:Strict mode胯究。
那主動聲明的全局變量如何釋放內(nèi)存了?答案就是主動將全局變量賦值成null:
function releaseLeak () {
window.leakObject = null;
}
下面是完整代碼1-global-variable.html:
<!DOCTYPE html>
<html>
<head>
<script>
'use strict';
function LeakObject () {
this.value = new Array(1024 * 1024).join('X');
}
function addLeak () {
window.leakObject = new LeakObject();
}
function releaseLeak () {
window.leakObject = null;
}
</script>
</head>
<body>
<button onclick="addLeak()">Add Leak</button>
<button onclick="releaseLeak()">Release Leak</button>
</body>
</html>
打開Chrome開發(fā)者工具躁绸,切換到Memory頁簽裕循,選擇“Record allocation timeline”,點擊開始净刮。然后多次點擊界面的“Add Leak"按鈕剥哑,你會發(fā)現(xiàn)藍色的內(nèi)存分配的進度條在移動。這是因為每次點擊按鈕后淹父,分配了1M的內(nèi)存(為了測試效果明顯new Array(1024 * 1024).join('X'))株婴,但覆蓋了之前全局變量的值,所以之前的藍色進度條變灰了暑认,也就是內(nèi)存被回收了困介。點擊左上角的紅色停止按鈕,然后在搜索欄輸入LeakObject蘸际,就會過濾出我們內(nèi)存泄漏的變量逻翁。最后可以刷新瀏覽器,重新來一次捡鱼,這此點擊網(wǎng)頁中的"Release Leak"按鈕試試,你會發(fā)現(xiàn)這次就沒有內(nèi)存泄漏了酷愧。
全局監(jiān)聽
全局監(jiān)聽是指加在window或者document對象上的監(jiān)聽驾诈,有兩種添加方式:
- window.on***,比如window.onresize
- window.addEventListener
下面是全局監(jiān)聽的示例2-global-listener:
<!DOCTYPE html>
<html>
<head>
<script>
'use strict';
function LeakObject () {
this.value = new Array(1024 * 1024).join('X');
}
function addLeak () {
var leakObject = new LeakObject();
window.onresize = function () {
console.log(leakObject.value.length);
};
}
function releaseLeak () {
window.onresize = null;
}
</script>
</head>
<body>
<button onclick="addLeak()">Add Leak</button>
<button onclick="releaseLeak()">Release Leak</button>
</body>
</html>
給window.onresize賦值的匿名函數(shù)中用到了變量leakObject溶浴,只要這個函數(shù)還有人引用(比如window.onresize)乍迄,leakObject不會被自動回收。想要修復內(nèi)存泄漏的問題士败,就需要將全局監(jiān)聽刪除闯两。對于window.onresize的形式褥伴,直接將window.onresize賦值為null;對于window.addEventListener的形式漾狼,需要將匿名函數(shù)用變量保存起來(比如listener)重慢,然后調(diào)用window.removeEventListener(listener)。
定時器
有兩類定時器:
- setInterval 一般用于定時請求后臺數(shù)據(jù)逊躁,刷新界面
- requestAnimationFrame 一般用于動畫似踱,IE10或以上版本才支持。雖然setInverval也能用于實現(xiàn)動畫功能稽煤,但requestAnimationFrame更有優(yōu)勢核芽,比如瀏覽器切換到其他Tab頁后,動畫會被暫停
定時器示例如下3-setInterval:
<!DOCTYPE html>
<html>
<head>
<script>
'use strict';
function LeakObject () {
this.value = new Array(1024 * 1024).join('X');
}
function addLeak () {
var leakObject = new LeakObject();
window._intervalId = setInterval(function () {
console.log(leakObject.value.length);
}, 1000);
}
function releaseLeak () {
clearInterval(window._intervalId);
window._intervalId = null;
}
</script>
</head>
<body>
<button onclick="addLeak()">Add Leak</button>
<button onclick="releaseLeak()">Release Leak</button>
</body>
</html>
計時器的回調(diào)函數(shù)中用到了leakObject酵熙,所以只要這個回調(diào)函數(shù)有人引用(window對象有引用)轧简,leakObject就不會被回收,修復方法是調(diào)用clearInterval匾二、cancelAnimationFrame清除定時器哮独。
定時器引起的內(nèi)存泄漏,在內(nèi)存快照中搜索不出來假勿,但從內(nèi)存分配中能看到借嗽。如下圖,點擊2次Add Leak按鈕后转培,內(nèi)存分配為2M恶导,但搜索不到LeakObject:
剛才的全局監(jiān)聽器和定時器都是由于閉包(Clousure)中引用了外部變量導致的內(nèi)存泄漏,這里有個更明顯的由于閉包引起的內(nèi)存泄漏的例子4-closure:
<!DOCTYPE html>
<html>
<head>
<script type="text/javascript">
'use strict';
function LeakObject () {
this.value = new Array(1024 * 1024).join('X');
}
var leakObject = null;
function addLeak() {
var oldObj = leakObject;
leakObject = {
leakObj: new LeakObject(),
closure: function () {
console.log(oldObj);
}
};
}
function releaseLeak () {
leakObject = null;
}
</script>
</head>
<body>
<button onclick="addLeak()">Add Leak</button>
<button onclick="releaseLeak()">Release Leak</button>
</body>
</html>
上面addLeak函數(shù)的代碼相當于如下代碼浸须,也就是構(gòu)造了一個對象鏈表:
function addLeak() {
var oldObj = leakObject;
leakObject = {
leakObj: new LeakObject(),
oldObj: oldObj
};
}
不停的調(diào)用addLeak函數(shù)后惨寿,會引發(fā)一連串的內(nèi)存泄漏:
最后,IE6删窒、7在對象相互引用時也會存在內(nèi)存泄漏裂垦,相信現(xiàn)在沒人用IE6、7了肌索,這里就不介紹了蕉拢。