給自己看的 JS 進階-Closure


Posted by 生菜 on 2020-10-23

給自己看的 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









Related Posts

VS code 段落過長怎麼辦?

VS code 段落過長怎麼辦?

D10_函式

D10_函式

C# 自訂Json檔

C# 自訂Json檔


Comments