你不知道的閉包原理【三個栗子徹底理解】

你不知道的閉包原理

想要理解閉包之前,就必須理解函數的創建過程、活動變量AO、作用域鏈。我曾寫過相關的文章

網上相關對閉包的定義:

  • MDN:函數和對其周圍狀態(lexical environment,詞法環境)的引用捆綁在一起構成閉包(closure)。也就是說,閉包可以讓你從內部函數訪問外部函數作用域。在 JavaScript 中,每當函數被創建,就會在函數生成時生成閉包。
  • 你不知道的JavaScript:是指有權訪問另外一個函數作用域中的變量的函數。創建閉包的常見方式就是在一個函數內部創建另外一個函數。
  • Javascript核心技術開發解密:閉包是一種特殊對象,由兩部分組成:執行上下文A + 該執行上下文創建的函數B

我對這些定義的理解:

  • 《MDN》的解釋更加接近原理,
  • 《你不知道的Javascript》的解釋更多講的是現象,
  • 《Javascript核心技術開發解密》的解釋更能說明閉包的真實存在:閉包是一種特殊對象。

———————————人工分割線——————————

下面我們通過幾個栗子來一步步講解閉包的原理:

栗子一:

    function makeFunc() {
        var name = "Mozilla";
        function displayName() {
            console.log(name);
        }
        return displayName;
    }

    var myFunc = makeFunc();
    myFunc();

打印結果:Mozilla

  • 混淆點1當執行到var myFunc = makeFunc();makeFunc函數在執行完之後裏面的name不是應該被垃圾回收機制給處理掉了嗎?
    答案:其實通過垃圾回收機制大概也知道name屬性不可能被回收,因為還有myFunc函數持有name的引用。
  • 混淆點2:與普通函數有什麼不同?
    答案:如果沒產生閉包,那麼函數中的臨時變量都被回收了。
  • 混淆點3:name屬性如果不回收,那麼存放在哪裡?
    答案:這個問題後面會講到
    【myFunc在google瀏覽器的內部屬性,生成了一個閉包對象makeFunc】
    在這裡插入圖片描述

先來思考一個問題:我們知道當執行了makeFunc函數後會產生對應的變量對象{變量對象保存了函數中的臨時變量},那麼上圖中的閉包對象makeFunc是否就等於makeFunc函數所產生的變量對象?

帶着這個問題看下一個栗子–

栗子二:

    function makeFunc() {
        var name = "Mozilla";
        var age = 12;
        function displayName() {
            console.log(name);
        }
        return displayName;
    }

    var myFunc = makeFunc();
    myFunc();

思考: 此時的閉包對象makeFunc是否帶有age屬性?
答案:沒有age屬性
在這裡插入圖片描述
說明:閉包對象不等於makeFunc的變量對象。閉包對象僅保存跨域的屬性。

延申到另一個常見問題:如何清除閉包?
我們知道閉包對象存在於myFunc函數內,所以一句:myFunc = null。使得閉包對象沒有引用持有那麼等待他的就是垃圾回收。

再延申到另一個常見問題:MDN:在 JavaScript 中,每當函數被創建,就會在函數生成時生成閉包。那麼豈不是內存很快就泄露了?
實際上你的閉包大多數都是沒有引用持有,很快就會被回收掉的。並且JS對閉包也有相關的優化處理。

然而:這個時候,我們是否明白這個閉包對象與作用域鏈的關係是什麼?

栗子三:

	var a = 20;
    function test () {
        var b = a + 10;
        function innerTest () {
        	debugger
            var c = 10;
            return b + c;
        }
        innerTest();
    }
    test();

??? 當執行到debugger時,此時innerTest函數的作用鏈是什麼呢?閉包對象是否產生?
:::此時innerTest函數的作用鏈:
在這裡插入圖片描述
閉包對象已經產生,並且閉包對象作為作用域鏈中的對象。

你是否記得很多書上都說作用域鏈是一條又每個函數的VO對象組成的鏈條。但這裡看到的卻不是VO對象,而是閉包對象。

我的看法是:如果單純從函數的作用域來看:作用域鏈是一條又每個函數的VO對象組成的鏈條。這個說法很正確,這是真正能夠以此幫助我們判斷訪問作用域邊界的依據。但是在程序實際的運行中,經過詞法編譯的階段,JS引擎已經通過代碼把各個實際上閉包產生的變量已經提煉出來。而不是直接就把VO對象放在作用域鏈。這也有利於提高訪問速度。

總結:

    function makeFunc() {
        var name = "Mozilla";
        function displayName() {
            console.log(name);
        }
        return displayName;
    }

    var myFunc = makeFunc();
    myFunc();

執行過程有關閉包的變化: 當調用makeFunc()函數進入函數創建階段時發現displayName函數含有name的跨函數變量,所以在對displayName函數進行提升的時候就已經給displayName函數初始化了閉包對象【makeFunc閉包】。所以當執行myFunc()函數的時候,從當前myFunc()的VO對象找不到的話就會從作用域鏈中的上一級【makeFunc閉包】對象中找。
來個圖清晰一點:
在這裡插入圖片描述
— 以上便是對閉包最新的理解。不對的望多多指出。