呼叫堆疊(五)-記憶體洩漏
四種常見的JS記憶體洩漏
1、意外的全域性變數
JavaScript處理未定義變數的方式比較寬鬆:未定義的變數會在全域性物件建立一個新變數。在瀏覽器中,全域性物件是window。
function foo(arg) {
bar = "this is a hidden global variable";
}
真相是:
function foo(arg) {
window.bar = "this is a hidden global variable";
}
函式foo內部忘記使用var,意外建立了一個全域性變數。
另外一種意外可能是this建立:
function foo () {
this.variable = "potential accidental global";
}
foo();
在 JavaScript 檔案頭部加上 'use strict',可以避免此類錯誤發生。啟用嚴格模式解析 JavaScript ,避免意外的全域性變數。
2、被遺忘的計時器或回撥函式
在JavaScript中使用setInterval非常平常。一段常見的程式碼:
var someResource = getData(); setInterval(function(){ var node = document.getElementById('Node'); if(node) { // 處理node和someResource node.innerHTML = JSON.stringify(someResource); } }, 1000)
此例說明了什麼:與節點或資料關聯的計時器不再需要,node 物件可以刪除,整個回撥函式也不需要了。可是,計時器回撥函式仍然沒被回收(計時器停止才會被回收)。同時,someResource 如果儲存了大量的資料,也是無法被回收的。
對於觀察者的例子,一旦它們不再需要(或者關聯的物件變成不可達),明確地移除它們非常重要。老的 IE 6 是無法處理迴圈引用的。如今,即使沒有明確移除它們,一旦觀察者物件變成不可達,大部分瀏覽器是可以回收觀察者處理函式的。
觀察者程式碼示例:
var element = document.getElementById('button'); function onClick(event) { element.innerHTML = 'text'; } element.addEventListener('click', onClick);
老版本的 IE 是無法檢測 DOM 節點與 JavaScript 程式碼之間的迴圈引用,會導致記憶體洩漏。如今,現代的瀏覽器(包括 IE 和 Microsoft Edge)使用了更先進的垃圾回收演算法,已經可以正確檢測和處理迴圈引用了。換言之,回收節點記憶體時,不必非要呼叫 removeEventListener 了。
3、脫離DOM的引用
有時,儲存 DOM 節點內部資料結構很有用。假如你想快速更新表格的幾行內容,把每一行 DOM 存成字典(JSON 鍵值對)或者陣列很有意義。此時,同樣的 DOM 元素存在兩個引用:一個在 DOM 樹中,另一個在字典中。將來你決定刪除這些行時,需要把兩個引用都清除。
var elements = {
button: document.getElementById('button'),
image: document.getElementById('image'),
text: document.getElementById('text')
};
function doStuff() {
image.src = 'http://some.url/image';
button.click();
console.log(text.innerHTML);
// 更多邏輯
}
function removeButton() {
// 按鈕是 body 的後代元素
document.body.removeChild(document.getElementById('button'));
// 此時,仍舊存在一個全域性的 #button 的引用
// elements 字典。button 元素仍舊在記憶體中,不能被 GC 回收。
}
4、閉包
function fn () {
var str = "aaaaa";
return function () {
console.log(str);
};
}
變數a被fn()函式內的匿名函式所引用,因此這種變數是不會被回收的。
記錄一下一開始自己的問題:
<body>
<div id="div">fasdf</div>
<script>
var list = []
var divs = document.getElementsByTagName("div")
list.push(divs[0])
document.body.removeChild(divs[0])
console.log(divs)
console.dir(list)
</script>
</body>
這裡我一開始認為陣列和變數是對同一個物件的引用,所以他倆應該都為空。
很明顯這是錯誤的。
首先我們應該把div這個標籤元素當做物件存在記憶體中,dom節點物件引用了它,所以在頁面中才顯示了出來。當我們通過let a = document.getElementById('div')獲取的時候,獲取到的直接是對記憶體中這個物件的引用。然後進行remove刪除操作的時候,是刪掉了dom節點物件對它的引用,而不會影響a對它的引用。
我們上面的程式碼var divs = document.getElementsByTagName("div"),獲取的divs是對dom節點物件的引用,而這個dom節點物件裡又有對div標籤元素的引用,當我們push操作時push的是對記憶體中div標籤元素的引用,所以remove之後,dom節點物件失去了對它的引用,結果顯示HTMLCollection[], 而陣列list並不為空。