---------------------這是學習筆記---------------------
隨著前端業務需求的不斷增多,相比以前,我們會佔用更多的記憶體。但是記憶體並不是無限的,而對於那些我們不再需要的變數、物件該怎麼處理呢?難道一個一個去手動釋放麼?其實並不需要,Javascript 具有自動垃圾回收機制,會定期對那些我們不再使用的變數、物件所佔用的記憶體進行釋放
Javascript 的垃圾回收機制
Javascript 會找出不再使用的變數,不再使用意味著這個變數生命週期的結束。Javascript 中存在兩種變數——全域性變數和區域性變數,全部變數的宣告週期會一直持續,直到頁面解除安裝
而區域性變數宣告在函式中,它的宣告週期從執行函式開始,直到函式執行結束。在這個過程中,區域性變數會在堆或棧上被分配相應的空間以儲存它們的值,函式執行結束,這些區域性變數也不再被使用,它們所佔用的空間也就被釋放
但是有一種情況的區域性變數不會隨著函式的結束而被回收,那就是區域性變數被函式外部的變數所使用,其中一種情況就是閉包,因為在函式執行結束後,函式外部的變數依然指向函式內的區域性變數,此時的區域性變數依然在被使用,所以也就不能夠被回收
function func1 () {
const obj = {}
}
function func2 () {
const obj = {}
return obj
}
const a = func1()
const b = func2()
複製程式碼
上面這個例子中,func1
執行時為 obj
分配了一塊記憶體,但是隨著函式執行結束,obj
佔用的空間也就被釋放了;而 func2
執行時,也為 obj
分配了記憶體,但是由於 obj
最終被返回賦值給了 b
導致其依然被使用,所以 func2
中的 obj
佔用的記憶體不會被釋放
垃圾回收的兩種實現方式
垃圾回收有兩種實現方式,分別是標記清除和引用計數
標記清楚
當變數進入執行環境時標記為“進入環境”,當變數離開執行環境時則標記為“離開環境”,被標記為“進入環境”的變數是不能被回收的,因為它們正在被使用,而標記為“離開環境”的變數則可以被回收
function func3 () {
const a = 1
const b = 2
// 函式執行時,a b 分別被標記 進入環境
}
func3() // 函式執行結束,a b 被標記 離開環境,被回收
複製程式碼
引用計數
統計引用型別變數宣告後被引用的次數,當次數為 0 時,該變數將被回收
function func4 () {
const c = {} // 引用型別變數 c的引用計數為 0
let d = c // c 被 d 引用 c的引用計數為 1
let e = c // c 被 e 引用 c的引用計數為 2
d = {} // d 不再引用c c的引用計數減為 1
e = null // e 不再引用 c c的引用計數減為 0 將被回收
}
複製程式碼
但是引用計數的方式,有一個相對明顯的缺點——迴圈引用
function func5 () {
let f = {}
let g = {}
f.prop = g
g.prop = f
// 由於 f 和 g 互相引用,計數永遠不可能為 0
}
複製程式碼
像上面這種情況就需要手動將變數的記憶體釋放
f.prop = null
g.prop = null
複製程式碼
在現代瀏覽器中,Javascript 使用的方式是標記清楚,所以我們無需擔心迴圈引用的問題
什麼是記憶體洩露?
常見的記憶體洩露案例
1)全域性變數照成記憶體洩露
function fn() {
name = "你我貸"
}
console.log(name)複製程式碼
在 JS 中處理未被宣告的變數, 上述範例中的會把 name , 定義到全域性物件中, 在瀏覽器中就是 window 上. 在頁面中的全域性變數, 只有當頁面被關閉後才會被銷燬. 所以這種寫法就會造成記憶體洩露, 當然在這個例子中洩露的只是一個簡單的字串, 但是在實際的程式碼中, 往往情況會更加糟糕.
另外一種意外建立全域性變數的情況.
function fn() {
this.name = "你我貸"
}
console.log(name)複製程式碼
在這種情況下this被指向了全域性變數 window, 意外的建立了全域性變數.
我們談到了一些意外情況下定義的全域性變數, 程式碼中也有一些我們明確定義的全域性變數. 如果使用這些全域性變數用來暫存大量的資料, 記得在使用後, 對其重新賦值為 null.
2)未銷燬的定時器和回撥函式照成記憶體洩露
function fn() {
return 2
}
var oTxt = fn();
setInterval(function() {
var oHtml = document.getElementById("test")
if(oHtml) {
oHtml.innerHTML = oTxt;
}
}, 1000); // 每 1 秒呼叫一次複製程式碼
如果後續 oHtml 元素被移除, 整個定時器實際上沒有任何作用. 但如果你沒有回收定時器, 整個定時器依然有效, 不但定時器無法被記憶體回收, 定時器函式中的依賴也無法回收. 在這個案例中的 fn 也無法被回收.
3 ) 閉包照成記憶體洩露
在 JS 開發中, 我們會經常用到閉包, 一個內部函式, 有權訪問包含其的外部函式中的變數. 下面這種情況下, 閉包也會造成記憶體洩露.
3)DOM 引用照成記憶體洩露
很多時候, 我們對 Dom 的操作, 會把 Dom 的引用儲存在一個陣列或者 Map 中.
var elements = {
txt: document.getElementById("test")
}
function fn() {
elements.txt.innerHTML = "1111"
}
function removeTxt() {
document.body.removeChild(document.getElementById('test'));
}
fn();
removeTxt()
console.log(elements.txt)複製程式碼
上述案例中, 即使我們對於 test 元素進行了移除, 但是仍然有對 test 元素的引用, 依然無法對齊進行記憶體回收.
另外需要注意的一個點是, 對於一個 Dom 樹的葉子節點的引用. 舉個例子: 如果我們引用了一個表格中的 td 元素, 一旦在 Dom 中刪除了整個表格, 我們直觀的覺得記憶體回收應該回收除了被引用的 td 外的其他元素. 但是事實上, 這個 td 元素是整個表格的一個子元素, 並保留對於其父元素的引用. 這就會導致對於整個表格, 都無法進行記憶體回收. 所以我們要小心處理對於 Dom 元素的引用.