什麼是閉包
參考資料:深入淺出瞭解 JavaScript 閉包(closure)
- 當你看到一個 function 內 return 了另一個 function,通常就是有用到閉包的概念。
- 在 function 內 return function,造成這種明明執行完畢卻還有東西被關住的現象,而這種情形就是一般人所熟知的閉包,Closure。
function test() {
var a = 10
function inner() {
console.log(a) // 10
}
inner()
}
test()
----
function test() {
var a = 10
function inner() {
console.log(a)
// inner() 可以記住上層的變數 10,
// 類似 鎖在 function 內的感覺
}
return inner
}
var func = test() // func 就等於是 return 的 inner
func() // inner() , 還是存取得到 a 的值
function foo() {
var a = 2;
function bar() {
console.log(a); // 這邊 a 會往上層找到 2
}
return bar;
}
var baz = foo(); // 設立一個變數來接 foo() 內部回傳的 bar
baz(); // 2
// 等於呼叫 bar()
說明:
在外面呼叫 baz()
也可以存取到 bar
的 scope,也就是可以存取到 foo scope 宣告的 a,也就是這個變數 a 就算被關住還是可以讓外面呼叫的 baz 存取到。
參考文章:你懂 JavaScript 嗎?#15 閉包(Closure)
文章中說明:
- 函式能夠存取的範疇即是其被內嵌後往外推的範圍,例如 bar 被內嵌於 foo 之內,因此 bar 內變數可存取的範圍就是 foo 和全域範疇。因此,基於語彙範疇的變數查找規則,bar 內的 a 在自己的函式範疇內找不到定義的話,可往外層的範疇查找,於是在 foo 內找到了,得到 a 為 2。其中,console.log(a) 是執行 RHS 查找。
- JavaScript 引擎的垃圾回收機制會釋放不再使用的記憶體,但閉包為了保留函式記得和存取其語彙範疇的能力,就會予以保留,不做記憶體回收。因此,bar 仍保留指向 foo 的內層範疇的參考,這個參考就是閉包。
- 最後,雖然 baz 位於 bar 所定義的範疇之外,但由於閉包的緣故,bar 仍能正常執行,而得到 a 的值。
閉包的應用
function complex(num) {
return num * num * num
}
function cache(func) {
var ans = {}
function inner(num) {
if (ans[num]) {
return ans[num]
}
ans[num] = func(num)
return ans[num]
}
return inner
}
const cachedComplex = cache(complex)
// 需要設立一個變數來接 cache(func) 回傳的 function
// cachedComplex() 就等於執行 cache(func) 內的 inner(num)
// 且可以存取到上層的 var ans = {}
console.log(cachedComplex(20))
// 第一次執行有進入 ans[20] = complex(20),輸出 8000
console.log(cachedComplex(20))
// 因為第一次已經計算過 ans[20] = 8000,已記住
// 所以不需再跑一次計算的 complex(20),直接輸出 8000
console.log(cachedComplex(20))
// 同上 8000
閉包的優點
- 把變數隱藏在裡面讓外部存取不到
- ```javascript=
var my_balance = 999
function deduct(n) {
my_balance -= (n > 10 ? 10 : n) // 超過 10 塊只扣 10 塊
}
deduct(13) // 只被扣 10 塊
my_balance -= 999 // 還是被扣了 999 塊
變數還是暴露在外部,任何人都可以直接來改這個變數。這時我們利用閉包來改寫,
```javascript=
function getWallet() {
var my_balance = 999
return {
deduct: function(n) {
my_balance -= (n > 10 ? 10 : n) // 超過 10 塊只扣 10 塊
}
}
}
var wallet = getWallet()
wallet.deduct(13) // 只被扣 10 塊
my_balance -= 999 // Uncaught ReferenceError: my_balance is not defined
因為把餘額這個變數給藏在 function 裡面,所以外部是存取不到的,也就是沒辦法直接從外部修改 my_balance 的值,想要修改只能夠利用已經暴露出去的 deduct 這個函式,這樣子就達到了隱藏資訊的目的,確保這個變數不會隨意地被改到。
閉包常見陷阱
var btn = document.querySelectorAll('button')
for(var i=0; i<=4; i++) {
btn[i].addEventListener('click', function() {
alert(i)
})
}
----
// 上面迴圈 i 的宣告就等於宣告在全域變數
var i
for(i = 0; i <= 4; i++) {
btn[i].addEventListener('click', function() {
alert(i)
})
}
此時按按鈕都會跳出 5,怪怪
因為迴圈是長這樣
btn[0].addEventListener('click', function() {
alert(i) // 這邊的 i 會存取到宣告在 global 的 i
})
btn[1].addEventListener('click', function() {
alert(i)
})
...
本來就是幫它加一個 function 是按下去的時候會跳出 i 而已,並沒有直接執行這個 function。(這邊是 event loop 概念)
而每一個迴圈的 alert(i)
會往上層找到 global 的 i,
那這個 i 的值會是什麼?因為按按鈕的時候迴圈已經跑完了,所以這時候 global 的 i 早已變成 5(迴圈的最後一圈,i 加一變成 5,判斷不符合 i<=4 這個條件所以跳出迴圈),畫面也就跳出數字 5 了。
- 解決方法 1:利用 closure 概念,加 function
function getAlert(num) {
return function() {
alert(num)
}
}
for(var i=0; i<=4; i++) {
btn[i].addEventListener('click', getAlert(i))
}
getAlert(i) 會「回傳」一個跳出 i 的 function,因此我額外產生了五個新的 function,每一個 function 裡面都有自己該跳出的值。
- 解決方法 2 :用 let const
for(let i=0; i<=4; i++) {
btn[i].addEventListener('click', function() {
alert(i)
})
}
因為 let 的特性,所以其實迴圈每跑一圈都會產生一個新的作用域,因此 alert 出來的值就會是你想要的那個值。
迴圈實際是這樣跑
{ // 塊級作用域
let i=0
btn[i].addEventListener('click', function() {
alert(i) // 這邊的 i 就會存取到 block 內部宣告的 i
})
}
{ // 塊級作用域
let i=1
btn[i].addEventListener('click', function() {
alert(i)
})
}
...
ECMAScript 中的作用域
以 ES3 為例
每個 EC (Execution Context)都有自己的 scope chain,當進入 EC 的時候 scope chain 會被建立。
當進入 EC 的時候,scope chain 會被初始化為 activation object 並加上 function 的[[Scope]]
這個屬性。
也就是在進入 EC 的時候會做下面這件事:
scope chain = activation object + [[Scope]]
- 什麼是 activation object(以下簡稱 AO)?
只有在進入「函式」的時候會產生這個 AO,而之後 AO 便被當作 VO 拿去使用。
什麼是 AO?可以把它直接當作 VO 的另外一種特別的型態,只在 function 的 EC 中出現,所以在 global 的時候我們有 VO,在 function 內的時候我們有 AO,但是做的事情都是一樣的,那就是會把一些相關的資訊放在裡面。
差別在哪裡?差別在於 AO 裡面會有一個arguments,其餘地方都是差不多的。
- 什麼又是
[[Scope]]
?
在建立 function 的時候會給一個 Scope,而這一個 Scope 會被設定到[[Scope]]去。
而在建立 function 時給的 Scope 就是當前 EC 的 Scope。
- 流程
- 當 function A 建立時,設置
A.[[Scope]] = scope chain of current EC
- 當進入一個 function A 時,產生一個新的 EC,並在這個 EC 設置
EC.scope_chain = AO + A.[[Scope]]
流程範例詳見參考資料:所有的函式都是閉包:談 JS 中的作用域與 Closure
額外補充:WTF is closure
var v1 = 10
function test() {
var vTest = 20
function inner() {
console.log(v1, vTest) //10 20
}
return inner
}
var inner = test()
inner()