什麼是閉包 (Closure)?


Posted by Kai on 2020-12-27

在 func1 中 return 另一個 func2

好處:在 func1 中的變數 可被 func2 使用,而 func1 的變數不會被外層讀取。
壞處:資料不會正常的被銷毀,而會一直留著。

執行的內部函式的方式會是 fun1()() 後方括弧表示呼叫內部函式,假如將函式賦予在另一個變數上,內部的變數就會存在內層,而隨著函式執行而不斷改變。
如同以下的範例:

function test() {
    let a = 10
    return function c(b) {
        a = a - b
        return a
    }
}
let d = test()
d(1) // 9
d(1) // 8
d(1) // 7

閉包的原理


那閉包的原理又是什麼呢?為什麼當中的變數會留著而記憶體不會被釋放呢?這時候就需要提到另一個東西了,叫做範圍鏈 (Scope Chain)

範圍鏈 (Scope Chain)

Execution Contexts (執行環境) 被建立時, Scope Chain、變數、 this 都被建立且初始化。

進入 function EC 的時候,範圍鏈的初始化會包含一個 Activation Object (AO) 以及 [[Scope]],function 的 [[Scope]] 是當被宣告時而決定的,也就是 scope chain 會是 [AO, [[Scope]] ]

Activation Object (AO)

當 function 被建立時,AO 被初始化,且會有一個 arguments 的屬性,而 AO 可以當作 VO 來使用,而 VO 就是前面所說的,在每個執行環境建立時都會有一個 Variable Object (VO),用於存放被宣告的變數及函式等。

    var a = 1
    function test() {
        var b = 2
        function test2() {
            var c = 3
            console.log(b)
            console.log(a)
        }
        test2()
    }
    test()

用以上的程式碼作為示範,整體的執行堆會像以下所寫的部分:

    test2 EC: {
        AO: {
            c:3
        },
        scopeChain: [test2EC.AO, test2.[[Scope]]]
        // [test2EC.AO, test2EC.[[Scope]]] = [test2EC.AO, testEC.scopeChain] = [test2EC.AO, testEC.AO, globalEC.VO]
    }
    test EC: {
        AO: {
            b: 2,
            test2: function
        },
        scopeChain: [testEC.AO, test.[[Scope]]]
        // [testEC.AO, testEC.[[Scope]]] = [testEC.AO, globalEC.VO] 
    }
    globalEC: {
        VO: {
            a: 1,
            test: function
        },
        scopeChain: [globalEC.VO]
    }

首先會初始化的是 Global EC

裡面會有一個 VO 並宣告變數 a = undefinedtest = func

再來建立自己的 Scope Chain 也就是 GlobalEC.VO,最後賦值 a = 1

呼叫 test() 於是,test EC 被建立,並疊在 Global EC 上,形成執行堆,

由於是函式,因此建立的會是 AO 並宣告變數 b = undefinedtest2 = func

建立 Scope Chain,函式的 Scope Chain 會是

testEC.AO, testEC.[[Scope]]

testEC.[[Scope]] = GlobalEC.scopeChain = GlobalEC.VO

也就是說,testEC.scopeChain = [testEC.AO, GlobalEC.VO]

之後賦值 b = 2

呼叫 test2() 依照同樣的步驟,test2EC.scopeChain

會是 test2EC.AO, [[Scope]]

等同於 test2EC.AO, testEC.scopeChain

再拆成 test2EC.AO, testEC.AO, globlaEC.scopeChain

最後變成 test2EC.AO, testEC.AO, globlaEC.VO

這樣就可以很清楚的看出來,test2() 執行後,log 出的資料會是 2 跟 1,用 letconst 也會是同樣的結果,因為範圍鏈的關係,所以內層的函式可以藉由外層的 VO/AO 而得到變數。

示範閉包


function test() {
    let a = 10
    return function c(b) {
        a = a - b
        return a
    }
}
let d = test()
d(1)

用一開始的函式做示範,他的執行堆就會如同以下的部分:

cEC:{
    AO: {
        b: undefined
    }
    scopeChain: [cEC.AO, testEC.AO, globalEC.VO]
}

testEC: {
    AO: {
        a: 10,
        c: function
    }
    scopeChain: [testEC.AO, globalEC.VO]
}
globalEC: {
    VO: {
        d: function,
        test: function
    }
    scopeChain: [globalEC.VO]
}

正常情況下,testEC.AO 在沒有被使用的情況下,記憶體會被釋放,但由於 function c 被 return,而且其範圍鏈中有 testEC.AO ,因此記憶體沒有被釋放,所以 a 這個變數才被保留下來,而這個就是閉包的原理。

有趣的是,以上是我們在文章上常看見對於閉包的解釋,但實際上在 JavaScropt 中,所有函式都可以被認為是閉包,主要的原因可以參考 Huli 大大所寫的 所有的函式都是閉包:談 JS 中的作用域與 Closure

以上筆記參考 Huli 的課程 [JS201] 進階 JavaScript:那些你一直搞不懂的地方


#closure #javascript #ScopeChain







Related Posts

k-means Clustering 介紹

k-means Clustering 介紹

學 JavaScript 的那些筆記 4--瀏覽器的 JS

學 JavaScript 的那些筆記 4--瀏覽器的 JS

從前端傳資料給後端(GET, POST)、從 PHP 連線到 MySQL 資料庫

從前端傳資料給後端(GET, POST)、從 PHP 連線到 MySQL 資料庫


Comments