【進階1-5期】JavaScript深刻之4類常見內存泄漏及如何避免

更新:謝謝你們的支持,最近折騰了一個博客官網出來,方便你們系統閱讀,後續會有更多內容和更多優化,猛戳這裏查看前端

------ 如下是正文 ------node

本期的主題是調用堆棧,本計劃一共28期,每期重點攻克一個面試重難點,若是你還不瞭解本進階計劃,文末點擊查看所有文章。webpack

若是以爲本系列不錯,歡迎點贊、評論、轉發,您的支持就是我堅持的最大動力。git


上篇文章詳細介紹了內存回收和內存泄漏,今天咱們繼續這個篇幅,不太重點是內存泄漏可能發生的緣由。es6

垃圾回收算法

經常使用垃圾回收算法叫作**標記清除 (Mark-and-sweep) **,算法由如下幾步組成:github

  • 一、垃圾回收器建立了一個「roots」列表。roots 一般是代碼中全局變量的引用。JavaScript 中,「window」 對象是一個全局變量,被看成 root 。window 對象老是存在,所以垃圾回收器能夠檢查它和它的全部子對象是否存在(即不是垃圾);web

  • 二、全部的 roots 被檢查和標記爲激活(即不是垃圾)。全部的子對象也被遞歸地檢查。從 root 開始的全部對象若是是可達的,它就不被看成垃圾。面試

  • 三、全部未被標記的內存會被當作垃圾,收集器如今能夠釋放內存,歸還給操做系統了。算法

現代的垃圾回收器改良了算法,可是本質是相同的:可達內存被標記,其他的被看成垃圾回收。跨域

四種常見的JS內存泄漏

劃重點 這是個考點

一、意外的全局變量

未定義的變量會在全局對象建立一個新變量,以下。

function foo(arg) {
    bar = "this is a hidden global variable";
}
複製代碼

函數 foo 內部忘記使用 var ,實際上JS會把bar掛載到全局對象上,意外建立一個全局變量。

function foo(arg) {
    window.bar = "this is an explicit global variable";
}
複製代碼

另外一個意外的全局變量可能由 this 建立。

function foo() {
    this.variable = "potential accidental global";
}

// Foo 調用本身,this 指向了全局對象(window)
// 而不是 undefined
foo();
複製代碼

解決方法

在 JavaScript 文件頭部加上 'use strict',使用嚴格模式避免意外的全局變量,此時上例中的this指向undefined。若是必須使用全局變量存儲大量數據時,確保用完之後把它設置爲 null 或者從新定義。

二、被遺忘的計時器或回調函數

計時器setInterval代碼很常見

var someResource = getData();
setInterval(function() {
    var node = document.getElementById('Node');
    if(node) {
        // 處理 node 和 someResource
        node.innerHTML = JSON.stringify(someResource));
    }
}, 1000);
複製代碼

上面的例子代表,在節點node或者數據再也不須要時,定時器依舊指向這些數據。因此哪怕當node節點被移除後,interval 仍舊存活而且垃圾回收器沒辦法回收,它的依賴也沒辦法被回收,除非終止定時器。

var element = document.getElementById('button');
function onClick(event) {
    element.innerHTML = 'text';
}

element.addEventListener('click', onClick);
複製代碼

對於上面觀察者的例子,一旦它們再也不須要(或者關聯的對象變成不可達),明確地移除它們很是重要。老的 IE 6 是沒法處理循環引用的。由於老版本的 IE 是沒法檢測 DOM 節點與 JavaScript 代碼之間的循環引用,會致使內存泄漏。

可是,現代的瀏覽器(包括 IE 和 Microsoft Edge)使用了更先進的垃圾回收算法(標記清除),已經能夠正確檢測和處理循環引用了。即回收節點內存時,沒必要非要調用 removeEventListener 了。

三、脫離 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 回收。
}
複製代碼

若是代碼中保存了表格某一個 <td> 的引用。未來決定刪除整個表格的時候,直覺認爲 GC 會回收除了已保存的 <td> 之外的其它節點。實際狀況並不是如此:此 <td> 是表格的子節點,子元素與父元素是引用關係。因爲代碼保留了 <td> 的引用,致使整個表格仍待在內存中。因此保存 DOM 元素引用的時候,要當心謹慎。

四、閉包

閉包的關鍵是匿名函數能夠訪問父級做用域的變量。

var theThing = null;
var replaceThing = function () {
  var originalThing = theThing;
  var unused = function () {
    if (originalThing)
      console.log("hi");
  };
    
  theThing = {
    longStr: new Array(1000000).join('*'),
    someMethod: function () {
      console.log(someMessage);
    }
  };
};

setInterval(replaceThing, 1000);
複製代碼

每次調用 replaceThingtheThing 獲得一個包含一個大數組和一個新閉包(someMethod)的新對象。同時,變量 unused 是一個引用 originalThing 的閉包(先前的 replaceThing 又調用了 theThing )。someMethod 能夠經過 theThing 使用,someMethodunused 分享閉包做用域,儘管 unused 從未使用,它引用的 originalThing 迫使它保留在內存中(防止被回收)。

解決方法

replaceThing 的最後添加 originalThing = null

PS:今晚弄到很晚,因爲時間問題,就再也不詳細介紹Chrome 內存剖析工具,有興趣的你們去原文查看。

週末彙總將在週日早上發送,週六會發送其餘類型的文章,敬請期待。

昨日思考題解答

問題一

從內存來看 null 和 undefined 本質的區別是什麼?

解答

給一個全局變量賦值爲null,至關於將這個變量的指針對象以及值清空,若是是給對象的屬性 賦值爲null,或者局部變量賦值爲null,至關於給這個屬性分配了一塊空的內存,而後值爲null, JS會回收全局變量爲null的對象。

給一個全局變量賦值爲undefined,至關於將這個對象的值清空,可是這個對象依舊存在,若是是給對象的屬性賦值 爲undefined,說明這個值爲空值

擴展下

聲明瞭一個變量,但未對其初始化時,這個變量的值就是undefined,它是 JavaScript 基本類型 之一。

var data;
console.log(data === undefined); //true
複製代碼

對於還沒有聲明過的變量,只能執行一項操做,即便用typeof操做符檢測其數據類型,使用其餘的操做都會報錯。

//data變量未定義
console.log(typeof data); // "undefined"
console.log(data === undefined); //報錯
複製代碼

null 特指對象的值未設置,它是 JavaScript 基本類型 之一。

null 是一個字面量,它不像undefined 是全局對象的一個屬性。null 是表示缺乏的標識,指示變量未指向任何對象。

// foo不存在,它歷來沒有被定義過或者是初始化過:
foo;
"ReferenceError: foo is not defined"

// foo如今已是知存在的,可是它沒有類型或者是值:
var foo = null; 
console.log(foo);	// null
複製代碼

問題二

ES6語法中的 const 聲明一個只讀的常量,那爲何下面能夠修改const的值?

const foo = {};

// 爲 foo 添加一個屬性,能夠成功
foo.prop = 123;
foo.prop // 123

// 將 foo 指向另外一個對象,就會報錯
foo = {}; // TypeError: "foo" is read-only
複製代碼

解答

const實際上保證的,並非變量的值不得改動,而是變量指向的那個內存地址所保存的數據不得改動。對於簡單類型的數據(數值、字符串、布爾值),值就保存在變量指向的那個內存地址,所以等同於常量。但對於複合類型的數據(主要是對象和數組),變量指向的內存地址,保存的只是一個指向實際數據的指針,const只能保證這個指針是固定的(即老是指向另外一個固定的地址),至於它指向的數據結構是否是可變的,就徹底不能控制了。所以,將一個對象聲明爲常量必須很是當心。

今日思考題

<script>
    console.log(fun)

    console.log(person)
</script>

<script>
    console.log(person)

    console.log(fun)

    var person = "Eric";

    console.log(person)

    function fun() {
        console.log(person)
        var person = "Tom";
        console.log(person)
    }

    fun()

    console.log(person)
</script>
複製代碼

上面代碼的執行結果是什麼?先本身分析,而後再到瀏覽器中執行。

參考

4類 JavaScript 內存泄漏及如何避免

ECMAScript 6 入門之const 命令

進階系列目錄

  • 【進階1期】 調用堆棧
  • 【進階2期】 做用域閉包
  • 【進階3期】 this全面解析
  • 【進階4期】 深淺拷貝原理
  • 【進階5期】 原型Prototype
  • 【進階6期】 高階函數
  • 【進階7期】 事件機制
  • 【進階8期】 Event Loop原理
  • 【進階9期】 Promise原理
  • 【進階10期】Async/Await原理
  • 【進階11期】防抖/節流原理
  • 【進階12期】模塊化詳解
  • 【進階13期】ES6重難點
  • 【進階14期】計算機網絡概述
  • 【進階15期】瀏覽器渲染原理
  • 【進階16期】webpack配置
  • 【進階17期】webpack原理
  • 【進階18期】前端監控
  • 【進階19期】跨域和安全
  • 【進階20期】性能優化
  • 【進階21期】VirtualDom原理
  • 【進階22期】Diff算法
  • 【進階23期】MVVM雙向綁定
  • 【進階24期】Vuex原理
  • 【進階25期】Redux原理
  • 【進階26期】路由原理
  • 【進階27期】VueRouter源碼解析
  • 【進階28期】ReactRouter源碼解析

交流

進階系列文章彙總:github.com/yygmind/blo…,內有優質前端資料,歡迎領取,以爲不錯點個star。

我是木易楊,網易高級前端工程師,跟着我每週重點攻克一個前端面試重難點。接下來讓我帶你走進高級前端的世界,在進階的路上,共勉!

相關文章
相關標籤/搜索