在 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 = undefined
及 test = func
再來建立自己的 Scope Chain 也就是 GlobalEC.VO,最後賦值 a = 1
。
呼叫 test()
於是,test EC 被建立,並疊在 Global EC 上,形成執行堆,
由於是函式,因此建立的會是 AO
並宣告變數 b = undefined
及 test2 = 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,用 let
或 const
也會是同樣的結果,因為範圍鏈的關係,所以內層的函式可以藉由外層的 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:那些你一直搞不懂的地方