這一章節給你們介紹的知識點相對比較簡單, 可是倒是很是重要的. 並且也是在面試過程當中常常會被問到的一部份內容.前端
經過這次閱讀你能夠學習到:web
其實在實際開發中, 咱們很容易不經意的就寫出內存泄露的代碼, 好比如下幾種狀況可能都是你遇到過的.面試
當咱們在一個函數中給一個變量賦值可是卻沒有聲明它時:算法
function fn () {
a = "Actually, I'm a global variable"
}
複製代碼
此時變量a
至關因而window
對象下的一個變量:數組
function fn () {
window.a = "Actually, I'm a global variable"
}
複製代碼
而以前咱們已經說了全局變量是很難被垃圾回收器回收的, 因此要是有這種意外的全局變量應該要避免.瀏覽器
this
建立的變量還有一種狀況是這樣的:閉包
function fn () {
this.a = "Actually, I'm a global variable"
}
fn();
複製代碼
咱們知道, 這裏this
的指向是window
, 所以此時建立的a
變量也會被掛載到window
對象下.編輯器
避免此狀況的解決辦法是在 JavaScript 文件頭部或者函數的頂部加上 'use strict'
, 開啓嚴格模式, 使得this
的指向爲undefined
, 這樣就能夠避免了.ide
固然若是你必須使用全局變量存儲大量數據時,確保用完之後把它設置爲 null 或者從新定義。函數
」
當咱們在代碼中使用定時器也有可能會形成內存泄露:
var serverData = loadData()
setInterval(function() {
var renderer = document.getElementById('renderer')
if(renderer) {
renderer.innerHTML = JSON.stringify(serverData)
}
}, 5000)
複製代碼
上面的例子🌰中, 節點renderer
引用了serverData
.在節點renderer
或者數據再也不須要時,定時器依舊指向這些數據。因此哪怕當renderer
節點被移除後,interval 仍舊存活而且垃圾回收器沒辦法回收,它的依賴也沒辦法被回收,除非終止定時器。
還有一個就是關於觀察者模式的案例:
var btn = document.getElementById('btn');
function onClick (element) {
element.innerHTMl = "I'm innerHTML"
}
btn.addEventListener('click', onClick);
複製代碼
對於上面觀察者的例子,一旦它們再也不須要(或者關聯的對象變成不可達),明確地移除它們很是重要。老的 IE 6 是沒法處理循環引用的。由於老版本的 IE 是沒法檢測 DOM 節點與 JavaScript 代碼之間的循環引用,會致使內存泄漏。
可是,現代的瀏覽器(包括 IE 和 Microsoft Edge)使用了更先進的垃圾回收算法(標記清除),已經能夠正確檢測和處理循環引用了。即回收節點內存時,沒必要非要調用 removeEventListener 了。
這種形成內存泄露的緣由簡單來講就是:
若是把DOM 存成字典(JSON 鍵值對)或者數組,此時,一樣的 DOM 元素存在兩個引用:一個在 DOM 樹中,另外一個在字典中。那麼未來須要把兩個引用都清除。
好比下面這個例子:
// 在對象中引用DOM
var elements = {
btn: document.getElementById('btn')
}
function doSomeThing () { elements.btn.click(); }
複製代碼function removeBtn () { // 將body中的btn移除, 也就是移除 DOM樹中的btn document.body.removeChild(document.getElementById('button')); // 可是此時全局變量elements仍是保留了對btn的引用, btn仍是存在於內存中,不能被GC回收 } 複製代碼
上面👆這種狀況, 能夠手動將引用給清除: elements.btn = null
.
還有一種狀況就是咱們以前提到過的閉包. 也就是局部變量銷燬時, 閉包的這種狀況.
首先讓咱們明確一點: 閉包的關鍵就是匿名函數可以訪問父級做用域中的變量.
來看一個簡單的例子🌰:
function fn () {
var a = "I'm a";
return function () {
console.log(a);
};
}
複製代碼
由於變量a
被fn()
函數內的匿名函數所引用, 所以這種變量是不會被回收的.
那就有人問了, 即便這樣會形成什麼不妥嗎? 在上面👆這個案例中固然看不出有什麼. 如果將狀況變得複雜一些呢?
var globalVar = null; // 全局變量
var fn = function () {
var originVal = globalVar; // 局部變量
var unused = function () { // 未使用的函數
if (originVal) {
console.log('call')
}
}
globalVar = {
longStr: new Array(1000000).join('*'),
someThing: function () {
console.log('someThing')
}
}
}
setInterval(fn, 100);
複製代碼
先請你花上一分鐘看看上面的案例, 你會發現:
fn
函數的時候都會產生一個新的對象
originVal
;
unused
是一個引用了
originVal
的閉包;
unused
雖然未被使用, 可是它引用的
originVal
迫使它留在內存中, 並不會被回收.
解決辦法是: 能夠在fn
的最底部, 將originVal
設置成null
.
上面👆介紹了這麼多種可能會形成內存泄露的狀況, 那麼有沒有什麼實際的辦法讓咱們看到內存泄露的表現呢?
固然是有的. 如今經常使用的是如下2種方式:
Chrome
瀏覽器的控制檯
Performance
或
Memory
Node
提供的
process.memoryUsage
方法
Chrome
瀏覽器的控制檯Performance
或Memory
Chrome 瀏覽器查看內存佔用,按照如下步驟操做。
Mac
快捷鍵
option+command+i
);
Performance
面板(不少教材中用的是
Timeline
面板, 不知道是否是版本的緣由);
Memory
, 而後點擊左上角的小黑點
Record
開始錄製;
Stop
結束錄製, 面板上就會顯示這段時間的內存佔用狀況。
若是內存的使用狀況一直在作增量, 那麼就是內存泄露了:
或者你可使用我在《記錄一次定時器及閉包問題形成的內存泄漏》中的方法進行檢查.
Node
提供的process.memoryUsage
方法另外一個就是Node
提供的process.memoryUsage
方法, 這一塊我用的不是不少, 這裏就貼上教材:
console.log(process.memoryUsage());
// { rss: 27709440,
// heapTotal: 5685248,
// heapUsed: 3449392,
// external: 8772 }
複製代碼
process.memoryUsage返回一個對象,包含了 Node 進程的內存佔用信息。該對象包含四個字段,單位是字節,含義以下:
rss(resident set size):全部內存佔用,包括指令區和堆棧。
heapTotal:"堆"佔用的內存,包括用到的和沒用到的。
heapUsed:用到的堆的部分。
external: V8 引擎內部的 C++ 對象佔用的內存。
複製代碼
判斷內存泄露, 是看heapUsed
字段.
總的來講, 常見的內存泄露包括:
如何避免內存泄露:
關於JavaScript
進階內存堆棧的內容就告一段落了, 總共是有五篇文章. 在文章中我也是儘可能用比較通俗的語言來讓進行講解, 但願你們都能搞懂.
若是你們喜歡霖呆呆的文章的話, 還請幫我一個小忙, 關注一波我最近纔開始搗鼓的公衆號, 我會在上面不按期的發一些關於前端方面的原創文章, 一塊兒學習, 一塊兒進步😊.