今天突然想到一個問題,let的塊級作用域,以及閉包的變數引用功能很有意思(這腦洞咋聯想到一起的,囧)。。閉包的使用會影響瀏覽器的GC過程。那麼:
- JS 物件什麼時候會被自動回收?
- 如何使用正確使用閉包,並避免記憶體洩漏?
先看一個經典例子,迴圈非同步列印問題(沒耐心的直接跳最後一個例項(^▽^))
// 想非同步列印1到5
for(var i=1; i<=5;i++) {
setTimeout(function(){
console.log("print: " + i);
}, i*1000)
}
// 結果
print: 6
print: 6
print: 6
print: 6
print: 6
複製程式碼
由於是非同步呼叫列印函式,所以等呼叫這個函式時,迴圈已經結束,i變成了6,所以連著列印5個6。
第二種情況,如果用let 來宣告i,let 和var 相比至少有如下特性:
- let宣告的變數擁有塊級作用域
- 形如for (let x...)的迴圈在每次迭代時都為x建立新的繫結(深度複製)
// 1到5
for(let i=1; i<=5;i++) {
setTimeout(function(){
console.log("print: " + i);
}, i*1000)
}
// 結果
print: 1
print: 2
print: 3
print: 4
print: 5
複製程式碼
這種情況下直接通過let, 實際上給每一次回撥函式的註冊,建立了一個閉包,所以列印正常。
第三種情況,通過手動建立閉包也可以實現類似效果。每次迴圈內,返回一個函式引用當時的變數 i,這樣實際上是重新分配了記憶體來儲存i 的值,而不是單純的引用記憶體地址。 尼瑪記憶體蹭蹭往上漲,不過這麼點資料完全不用擔心
// 1到5
for(var i=1; i<=5;i++) {
setTimeout((function(){
var b = i; //install timer的時候引用 i 並且return 一個函式
return function(){
console.log("print: " + b);
}
})(), i*1000)
}
複製程式碼
這個例子很好地說明了閉包對內部變數記憶體地址的保留作用(迴圈1w次就深度複製了1w份i )。但閉包和全域性變數的不當使用可能會導致記憶體洩漏,記憶體居高不下甚至標籤頁直接掛掉。
JS 變數在瀏覽器記憶體中是否被GC 回收要看這個變數所在作用域的生命週期和變數是否被別人引用:
- 如果是函式內部宣告的變數,並且沒有任何外部變數引用,則函式執行完就銷燬。如果有引用,則該內部變數會一直遊離於記憶體中
JS 物件(引用型別)是儲存在記憶體堆heap中,可以通過Chrome Debug Tool的 Profile 工具生成Heap SnapShot 來檢視。
最後看一個活生生的例項,不出意外分分鐘記憶體佔用1G:
function Test()
{
this.obj= {};
this.index = 1;
this.timer = null;
var cache = []; // 內部變數,記憶體隱患...
this.timer = window.setInterval(() =>{
this.index += 1;
this.obj = {
val: '_timerxxxxxbbbbxx_' + this.index,
junk: [...cache]
};
cache.push(this.obj);
}, 1);
console.warn("create Test instance..");
}
test = new Test(); // JS物件開啟定時器不斷分配記憶體
複製程式碼
囉嗦幾句,這個例子的關鍵在於內部變數cache被外部的非同步函式(定時器)引用。 如果不清除定時器,只是把Test類的例項手動設為null,也無濟於事,cache還會繼續佔用記憶體。
Test.prototype.destroy = function(){
clearInterval(this.timer);
}
function d() {
// 取消定時器並銷燬Test 例項
test.destroy();
test = null;
console.warn("destroyed test instance..");
}
複製程式碼
清除內部變數cache的引用後,記憶體堆大小立刻下降了40MB.
總結:
- 函式內部不用的區域性變數及時清理,在清理時要考慮ta的所有引用函式。
- 非得引用區域性變數,請用非匿名函式,否則難以銷燬引用。
個人見解,說得不對之處歡迎評論指正 : )
參考文章: