前端學習筆記——關於閉包與內存泄漏

這個前端學習筆記是學習gitchat上的一個課程,這個課程的質量非常好,價格也不貴,非常時候前端入門的小夥伴們進階。
在這裏插入圖片描述
筆記不會涉及很多,主要是提取一些知識點,詳細的大家最好去過一遍教程,相信你一定會有很大的收穫

作用域

就近原則,通過作用域鏈,找到最近的一個變量

  1. ES6的塊級作用域

    使用letconst命名的變量,不會聲明提升。

    在上述2個聲明的變量前使用變量會報錯,並且同一個作用域下只允許一個let聲明同一個變量。

  2. 函數的參數也會出現死區TDZ,所以需要注意引用的順序。

執行上下文和調用棧

  1. 執行上下文就是當前代碼的執行環境/作用域

代碼執行會經歷2個階段:

  1. 代碼預編譯(VO)

    降JS代碼編譯成可執行的代碼。javascript是解釋型語言,編譯一行執行一行。執行前,javascript引擎會提前做些準備工作。確認語法無誤的時候,javascript代碼在預編譯的階段對變量的內存空間進行分配,在這過程中會經歷:

    1. 變量聲明
    2. 變量聲明提升,值爲undefined
    3. 非表達式函數聲明提升
  2. 代碼執行階段(AO)

    此時作用域鏈已經確定,由當前作用域和外層所決定,保證了變量的有序訪問。

調用棧

函數一個接着一個調用,形成的類似棧訪問。符合先進後出的形式。

函數執行完之後,退出棧之後,函數的內部變量就會被垃圾回收器回收,這也是函數外部無法訪問函數內部的原因。

正常的函數會經歷3個階段:

  1. 創建階段
  2. 執行階段
  3. 執行完畢,回收階段

閉包

閉包就是爲了解決無法訪問函數內部的這個問題,函數執行完畢,空間不會馬上銷燬。

內層函數引用了外層函數作用域下的變量,並且內層函數在全局環境下可訪問,就形成了閉包。

一個簡單的閉包:

function numGenerator() {
    let num = 1
    num++
    return () => {
        console.log(num)
    } 
}

var getNum = numGenerator()// 函數執行之後,num不會馬上消失
getNum()// 可以通過返回的函數進行訪問

我們如果返回一個函數,而這個函數訪問了上一個函數作用域下的變量,這樣就能夠讓函數上下文在函數執行之後不會馬上銷燬。這也是閉包的基本原理。

內存管理

  1. 分配內存:聲明的時候,會劃分內存保存數據
  2. 使用內存:所有使用到變量的地方
  3. 銷燬內存:手動等於null,或者執行完畢之後走出上下文環境等

堆內存和棧內存

javascript有2種數據類型:基本和引用

基本類型都保存在棧內存中,而引用類型保存在堆內存中,注意引用內存也會使用到棧內存,引用類型棧內存保存的是堆內存的地址。這裏用一張圖表示:

在這裏插入圖片描述
每一個語言都會有屬於自己的垃圾回收器,在不用到的內存會進行釋放,但是垃圾回收器也是不完美的,也會存在判斷錯誤的情況,無法將垃圾內存進行回收。這樣就會出現內存泄漏:指內存空間明明已經不再被使用,但由於某種原因並沒有被釋放的現象。

內存泄漏

這個是每一個程序員都需要關心的問題。我們來看看前端有那些常見的內存泄漏情形。

  1. 例子1:

    var element = document.querySelectorAll("li")
    
    

// 移除 element 節點
function remove() {
document.querySelector(‘body’).removeChild(element1[0])
}
```

我們在操作DOM元素的時候,如果響應移除掉一個元素節點,一般使用removeChild即可,但是上述情況中,element還保存着對已經移除的元素引用,雖然視覺上這個節點已經移除了,但是在dom對象上,還是會保存這個節點的信息。所以,我們需要手動的移除element這個節點。

在這裏插入圖片描述

  1. 例子2

    var element = document.getElementById('element')
    element.innerHTML = '<button id="button">點擊</button>'
    
    var button = document.getElementById('button')
    button.addEventListener('click', function() {
        // ...
    })
    
    element.innerHTML = ''
    

    這裏我們動態添加一個節點,但是我們給這個按鈕添加了一個事件之後,想要清楚element節點的內容,雖然視覺上已經移除,但是在內存中,button還保存着button節點的信息和其事件處理句柄還在,垃圾回收器無法回收,需要手動清楚這些引用。

  2. 例子3

    定時器,如果不需要了就及時停止。

    function foo() {
      var name  = 'lucas'
      window.setInterval(function() {
        console.log(name)
      }, 1000)
    }
    
    foo()
    

    由於計時器的一直存在,name無法釋放內存。

    如果業務不需要,就自己手動停止計時器。clearInterval

  3. 意外的全局變量

    function foo(arg) {
        bar = "this is a hidden global variable";
    }
    // 或者不恰當使用this
    function foo() {
        this.variable = "potential accidental global";
    }
    

    函數執行後,並不會消除bar的內存

垃圾回收機制

當然,除了開發者主動保證以外,大部分的場景瀏覽器都會依靠:

  • 引用計數

    這是一個算法,能夠追蹤沒有被引用的對象,進行回收。通過例子來理解這個引用的意思。

    var obj1 = {
        property1: {
             subproperty1: 20
         }
    };
    
    

    obj1引用了一個對象property1,property1也引用了一個對象。由於obj1引用了一個對象,所以不會被回收。

    我們接下來繼續進行操作:

    var obj2 = obj1;
    
    obj1 = "some random text"
    
    

    這時,obj2引用這obj1所引用的同一個對象,後面obj1不在引用對象,這時只存在一個obj2引用着這個對象。垃圾回收器也不會回收。

    我們繼續操作:

    var obj2_pro = obj2.property1
    

    obj2_pro引用這obj2屬性。這個時候,對象就有2個引用,一個是obj2,一個是obj2_pro

    我們消除obj2的引用

    obj2 = "some random text"
    

    但是還存在obj2_pro的引用,垃圾回收器無法回收。我們繼續操作,消除obj2_pro的引用。

    obj2_pro = null
    

    這個時候,最初的對象沒有任何引用了,這時引用垃圾回收器就可以進行回收了。

    假如,2個對象互相引用,那麼這個就永遠無法回收了。這就會導致內存泄漏。

  • 標記清除

    爲了解決引用計數的弊端,這裏還存在一種回收機制。

    我們從跟節點進行訪問,把所有訪問到的進行標記,所有可以遍歷都遍歷之後,存在沒有被標記的對象,就可以進行回收。

    自 2012 年以來,JavaScript 引擎已經使用此算法來代替引用計數垃圾回收。

參考文章
通過垃圾回收機制理解 JavaScript 內存管理
四種常見的內存泄漏

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章