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

Chucker --- View the interaction between the app and the api

Chucker --- View the interaction between the app and the api

UML 類別圖

UML 類別圖

2356. Number of Unique Subjects Taught by Each Teacher

2356. Number of Unique Subjects Taught by Each Teacher


Comments