閉包是在 function 裡面 return function
當 function 執行結束之後,會將資源釋放,但以下面例子而言,在閉包裡面依舊存取得到 a
function test(){
var a = 10
function inner() {
a++
console.log(a)
}
return inner
}
var func = test()
func() // inner()
func()
func()
閉包的實際應用
共用同一個 ans 並把值記住
function complex(num) {
console.log('calc')
return num * num * num
}
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)) // 計算
console.log(cacheComplex(30)) // 計算
console.log(cacheComplex(20)) // 不用再次計算,已有結果
console.log(cacheComplex(30)) // 不用再次計算,已有結果
// calc
// 8000
// calc
// 27000
// 8000
// 27000
從 ECMAScript 來解釋 VO, AO
- 每一個 Excution Context 都有一個 scope chain
- 當進入新的 Excution Context,scope chain 就會被建立並初始化。
- 當進入 function code 的時候,scope chain 會新增 activation object (AO),還有會新增預設的屬性 arguments。AO 可當作 variable object(VO) 來用。
global EC 裡面有 VO。
global EC: { VO: { } }
- function EC 裡面有 AO
function EC: { AO: { a: undefined, func: func, } scopeChain: [function EC.AO, [scope]] // 在 function 被宣告的時候決定 scopeChain // scope chain 是自己的 AO 加上 scope 的 property }
enter function EC =>
scope chain: [AO, [[scope]]
範例
程式碼
var a = 1
function test() {
var b = 2
function inner() {
var c = 3
console.log(b)
console.log(c)
}
inner()
}
test()
scope chain 解釋
innerEC: {
AO: {
c: 3,
},
scopeChain: [innerEC.AO, inner.[[Scope]]]
= [innerEC.AO, test.[[Scope]]]
= [innerEC.AO, testEC.AO, globalEC.VO]
}
testEC: {
AO: {
b: 2,
inner: function
},
scopeChain: [testEC.AO, test.[[Scope]]]
= [testEC.AO, globalEC.VO]
}
inner[[Scope]] = testEC.scopeChain
= [testEC.AO, globalEC.VO]
globalEC: {
VO: {
a: 1,
test: func
},
scopeChain: [globalEC.VO]
}
test.[[Scope]] = globalEC.scopeChain
閉包是 scope chain 有 reference 其他 excution context 的 VO、AO
因 return 之後,只要 scope chain 有連結到,還是可能用到 AO 或 VO,所以 JS 的垃圾回收機制就不會將其回收。
閉包的優點
- 變數隱藏在裡面讓外部存取不到
- 當輸入的資料曾經計算過,不須重新計算一次。
閉包的問題
有可能保留到不想保留的值
var v1 = 10
function test() {
var vTest = 20
var obj = { hugeObject: 'huge object' }
function inner() {
console.log(v1, vTest, obj.hugeObject)
}
return inner
}
var inner = test()
inner()
不同角度看閉包
閉包:「會記住週邊資訊的 funciton」
這樣在 JS 裡面,所有 function 都是閉包,,因都會把上層的資訊記起來,但通常不會這樣定義閉包。
閉包:function 裡面會 return function
這才是一般所定義的閉包
閉包:作用域陷阱
這樣等於在全域宣告 i,當 for 迴圈裡面的 function 往上找 i 時,迴圈已經跑完了,i 已經等於 5。
var arr = []
for (var i=0; i<5; i++) {
arr[i] = function() {
console.log(i)
}
}
arr[0]() // 5
解決方法(一)
用新的 funciton 代替,因建立新的 function 故有新的作用域記住 i 值
var arr = []
for (var i=0; i<5; i++) {
arr[i] = logN(i)
}
function logN(n) {
return function() {
console.log(n)
}
}
arr[1]() // 1
解決方法(二):IIFE
var arr = []
for (var i=0; i<5; i++) {
arr[i] = (function (n) {
return function() {
console.log(n)
}
})(i)
}
arr[1]()
解決方法(三):let
var arr = []
for (let i=0; i<5; i++) {
arr[i] = function() {
console.log(i)
}
}
arr[0]()
因 let 的變數生存範圍在 block 裡,跑迴圈時每跑一圈便會產生新的 block scope。
相當於以下程式碼:
{
let i = 0
arr[0] = function() {
console.log(i)
}
}
{
let i = 1
arr[1] = function() {
console.log(i)
}
}
{
let i = 2
arr[2] = function() {
console.log(i)
}
}
arr[0]()
arr[2]()
closure 可避免變數被外部修改
function createWallet(initMoney) {
var money = initMoney
return {
add: function(num) {
money += num
},
deduct: function(num) {
num >= 10 ? (money -= 10) : (money -= num)
},
getMoney() {
return money
}
}
}
var myWallet = createWallet(99)
myWallet.add(1)
myWallet.deduct(10)
console.log(myWallet.getMoney())