給自己看的 JS 進階:(建議按照順序看)
給自己看的 JS 進階-變數
給自己看的 JS 進階-Hoisting
給自己看的 JS 進階-Closure
給自己看的 JS 進階-物件導向
Closure 閉包
先看一個例子:
function test() {
var a = 10
function inner() {
a++
console.log(a)
}
return inner // 回傳 inner 這個函數
}
var func = test()
fanc() // 也就是 inner(), 11
fanc() 12
fanc() 13
所謂 Closure 閉包
就是像這樣,在一個 function 中 return 一個 function 。當我們呼叫裡面的 function 時,裡面的 function 會將外面 finction 的值記起來並鎖在裡面,因此稱為閉包。
如果我們希望能記住上次計算的值,不用再算一次,就可以使用閉包,例如:
function complex(n) {
// 複雜計算
console.log('caculate')
return n * n
}
function cache(func) {
var ans = {}
return function(num) {
if(ans[num]) { // 如果有紀錄就直接回傳值
return ans[num]
}
ans[num] = func(num)
return ans[num]
}
}
const cacheComplex = cache(complex)
console.log(cacheComplex(20)) // caculate 400
console.log(cacheComplex(20)) // 400
console.log(cacheComplex(20)) // 400
只要算過一次 ans[num]
就會被記起來,之後就都不用再跑一次 complex 了。
Closure ㄉ原理
ECMAScript ES3 版本中有提到,每個 CE 都有一個 Scope Chain
,進入 EC 時, Scope Chain
被初始化為 Activation Object
(其實就是 function 中的 VO
) 和 [[scope]]
。
看一個簡單的例子:
var a = 1
function test() {
var b = 2
function inner() {
var c = 3
console.log(a) // 1
console.log(b) // 2
}
inner()
}
test()
此時底層的狀態是:
Global EC: {
VO: {
a: undefined,
test:func
},
scopeChain: [Global VO]
}
// 初始化一下
test.[[Scope]] = globalEC.scopeChain // [Global.VO]
進到 testEC 之後如下:
test EC: {
AO: {
b: undefined,
inner: func
},
scopeChain: [testEC.AO, test.[[Scope]]]
// 看上一格,也就是 [testEC.AO, globalEC.scopeChain]
// 也就是 [testEC.AO, Global.VO]
}
// 初始化一下
INNER.[[Scope]] = testEC.scopeChain // [testEC.AO, Global.VO]
Global EC: {
VO: {
a: 1,
test: func
},
scopeChain: [Global.VO]
}
最後進入 innerEC:
inner EC: {
AO: {
c: undefined,
},
scopeChain: [innerEC.AO, inner.[[Scope]]]
// 也就是 [innerEC.AO, testEC.AO, Global.VO]
}
test EC: {
AO: {
b: 2,
inner: func
},
scopeChain: [testEC.AO, test.[[Scope]]]
// 也就是 [testEC.AO, Global.VO]
}
Global EC: {
VO: {
a: 1,
test: func
},
scopeChain: [Global.VO]
}
此時回到一開始的例子:
function test() {
var a = 10
function inner() {
a++
console.log(a)
}
return inner // 回傳 inner 這個函數
}
var func = test()
fanc() // 也就是 inner()
最後一行執行時 test EC
已經結束,本來底層機制應該要全部拿掉,但因為 innerEC.scopeChain
是 [innerEC.AO, testEC.AO, Global.VO]
,因此 testEC.AO
還不能那麼快退場。這就是為什麼 inner 可以拿到上一層變數值並儲存更改的原因。
不過偶爾閉包也會產生一些問題,例如外層包了超大的物件,就算之後只使用內層,因為關聯外層的 AO ,那個超大物件就無法被回收。
Closure 的小陷阱
var arr = []
for (var i = 0; i < 5; i++) {
arr[i] = function() {
console.log(i)
}
}
arr[0]()
本來預期會得到一到五,結果出來卻只有 5 。
此處的 i 是一個 global 的變數,當我們呼叫 arr[0]()
時,是進到 for 迴圈中拿函數,因此函數中的 scope chain 會連動到 global 的 AO ,呼叫時 for 迴圈已經跑完,所以 global.VO
中 i 的值是 5 ,延用該 VO 的函數自然而然會輸出 5 。
解決方法:
function logN(n) { // 閉包的概念
return function() {
console.log(n)
}
}
const log2 = logN(2)
log2() // 2
var arr = []
for (var i = 0; i < 5; i++) {
arr[i] = logN(i)
}
arr[0]() // 0
因為 arr[i]
會迴傳一個新的 function ,因此會產生新的作用域去記住傳入的值。
也可以使用 IIFE
,也就是立即呼叫函式,例如:
(() => {
console.log(123)
})() // 立刻執行,輸出 123
回到剛剛的問題,我們也可以把剛剛的函式 logN
放進去:
var arr = []
for (var i = 0; i < 5; i++) {
arr[i] = (function(num) {
return function() {
console.log(num)
}
})(i)
}
arr[0]() // 0
也可以直接這樣寫:
var arr = []
for (let i = 0; i < 5; i++) {
arr[i] = function() {
console.log(i)
}
}
arr[0]()
因為 let
的作用域只存在 block 中,迴圈等於是跑了五個 block arr[i] = function() { console.log(i) }
,每一圈都有自己的作用域,所以呼叫 arr[0]
時自然就找到印出 0 的函數。
Closure 的範例
Closure 隱藏資訊不被額外操控的時候很好用,例如:
var money = 99
function add(num) {
money += num
}
function deduct(mun) {
money -= num
}
add(1)
deduct(10)
console.log(money) // 90
// 這時你同事很壞,加了一行
money -= 90
// 就算繞過任何操作還是可以改變 money 的值
此時就可以使用 Closure:
function createWallet(init) {
var money = init
return {
add: function(num) {
money += num
},
deduct: function(num) {
money -= num
}
}, getMoney() {
return money
}
}
var myWallet = createWallet(99)
myWallet.add(1)
myWallet.deduct(10)
console.log(myWallet.getMoney()) // 90