JavaScript 進階 03:Closure


Closure 閉包:function 裡面回傳了一個 function

  • 舉個例子
function test() {
    var a = 10
    function inner() {
        a++
        console.log(a) //  a = 11
    }
    inner()
}
test()
  • 改成 return
function test() {
    var a = 10
    function inner() {
        a++
        console.log(a)
    }
    return inner // 把這個 inner function 回傳
    // 若寫成 return inner() 是執行 function
}
var func = test() // 接住 inner function
func() // 有了 inner function 就能去呼叫 inner(),a = 11
func() // a = 12
func() // a = 13
  • 再舉個例子
function complex(num) {
// 複雜計算
console.log('calculate')
return num * num * num
}
function cache(func) {
    var ans = {}
    return function(num) {   
        if(ans[num]) {
            return ans[num]
        }
        ans[num] = func[num] // 第一次執行:ans[20] = complex(20)
        return ans[num]
    }
}
const cachedComplex = cache(complex)
console.log(cachedComplex(20))
console.log(cachedComplex(20))
console.log(cachedComplex(20))
回傳值
calculate
8000
8000
8000
// 把 complex function 傳進去

了解 Closure 之前,要先熟悉 Scope Chain

每個 EC 都會有一個 scope chain,當進入 EC 的時候 scope chain 會被建立。

The scope chain is initialised to contain the activation object followed by the objects in the scope chain stored in the [[Scope]] property of the Function object.

當進入 EC 的時候,scope chain 會被初始化為 activation object 並加上 function 的[[Scope]]這個屬性。

緊接著,我們要來解釋 activation object 與 [[Scope]] 是什麼?

  • Activation Object
    當我們進入函式的時候,即會產生 AO。接著,AO 就會被當作 VO 來使用。AO 即是 VO 的另外一種型態,只有在函式的 EC 出現。因此在 global 的 EC 時,我們會有 VO,在函式的 EC 時,我們會有 AO。那麼究竟兩者的差別在哪裡?差在 AO 裡面會有一個arguments,EO 沒有。

  • [[Scope]]
    建立函式的時候會給一個 Scope,而這一個 Scope 會被設定到[[Scope]]裡面。

    1. 當 function A 建立時,設置A.[[Scope]] = scope chain of current EC
    2. 當進入一個 function A 時,產生一個新的 EC,並設置 EC.scope_chain = AO + A.[[Scope]]
  • 舉個例子時間

var a = 1
function test() {
    vat b = 2
    function inner() {
        var c = 3
        console.log(b)
        console.log(a)
    }
    inner()
}
test()
inner EC: {
    AO: {
        c: 3
    },
    scopeChain:[innerEC.AO, inner.[[Scope]]]
    = [innerEC.AO, testEC.scopeChain]
    = [innerEC.AO, testEC.AO, globalEC.VO]
}
===
test EC: {
    AO: {
        b: 2
        inner: func
    },
    scopeChain: [textEC.AO, test.[[Scope]]]
    => [testEC.AO, globalEC.VO]
}
inner.[[Scope]] = testEC.scopeChain
=> [testEC.AO, globalEC.VO]
===
Global EC: {
    VO: {
         a = 1,
        test = func
    },
    scopeChain: [globalEC.VO]
}
test.[[Scope]] = globalEC.scopeChain // globalEC.VO
  • 假裝自己是 JS 引擎,來解釋 Closure
var v1 = 10
function test() {
    var vTest = 20
    function inner() {
        console.log(v1, vTest)
    }
    return inner
}
var inner = tets()
inner()
test EC {
    AO: {
        vTest: undefined,
        inner: func
   },
   scopeChain: [testEC.AO, globalEC.VO]
}
inner.[[Scope]] =  [testEC.AO, globalEC.VO] 
===
global EC {
    VO: {
        v1: 10,
        inner: undefined,
        test : func
    },
    scopeChain: [globalEC.VO]
}
test.[[Scope]] = [globalEC.VO]
inner EC: {
    AO: {
    },
    scopeChain: [innerEC.AO, testEC.AO, globalEC.VO]
}
===
global EC {
    VO: {
        v1: 10,
        inner: undefined,
        test : func
    },
    scopeChain: [globalEC.VO]
}
===
test EC {
    AO: {
        vTest: 20,
        inner: func
   },

// 因為還會用到, 所以不能被回收掉

日常常見的作用域陷阱

  • 聽說是 JS 面試萬年考古題
var arr = []
for (var i = 0; i< 5; i++) {
    arr[i] = function() {
        console.log(i)
    }
}
arr[0]()
// 印出的值為 5
var arr = []
var i
for(i = 0; i< 5; i++) {
    arr[i] = function() {
        console.log(i)
    }
}
arr[0]()
===
arr[0] = function() {
    console.log(i)
}
arr[1] = function() {
    console.log(i)
}
...
//迴圈跑完產生的 5 個 function,i = 5 不符合跳出
//接著往上一層 GlobalEC i 的值
  • 解決方式 1: 閉包的使用
var arr = []
for(var i = 0; i< 5; i++) {
    arr[i] = logN(i) // 傳進去的值
    }
function logN(n) {
    return function() {
        console.log(n)
    } // 回傳一個新的 function
}
arr[0]()
// 新建一個新的 function
// 會有一個新的作用域,去記住 i 的值
===
// 迴圈跑五圈
arr[0] = logN(0) 印出 0
arr[1] = logN(1) 印出 1
arr[2] = logN(2) 印出 2
arr[3] = logN(3) 印出 3
arr[4] = logN(4) 印出 4
  • 解決方式 2:IIFE (立即呼叫函式)
var arr = []
for(var i = 0; i< 5; i++) {
    arr[i] = (function() {
        return function() {
            console.log(number)
        }
    })(i)
}
arr[0]()
// 立即呼叫函式,可讀性比較差
  • 解決方式 3: var 改成 let
var arr = []
for(let i = 0; i< 5; i++) {
    arr[i] = function() {
        console.log(i)
    }
}
arr[0]()
// let 的作用域為 block chain
===
{
    let i = 0
    arr[0] = function() {
        console.log(i)
    }
}
{
    let i = 1
    arr[1] = function() {
        console.log(i)
    }
}
...
  • 解決方式 4:變數宣告為 var,在 setTimeout() 傳參數 i 進去
for(var i=0; i<5; i++) {
    setTimeout((i)=> {
        console.log(i)
    }, 1000, i)
}
// 變數宣告為 var,在 setTimeout() 傳參數 i 進去,也會印出 0, 1, 2, 3, 4

Closure 可以應用在哪裡?

用到 Closure 的地方,會是想要隱藏某些資訊

var money = 99
function add(num) {
    money += num
}
function deduct(num) {
    if(num >= 10) {
        money -= 10
    } else {
        money -= num
    }
}
add(1)
deduct(100)
money = 20 // 可以任意調整 money 的值
console.log(money)
function createWallet(initMoney) { // 初始值
    var money = initMoney //  宣告變數保留初始值
    return {
        add: function(num) {
            money += num
        },
        deduct: function(num) {
            if(num >= 10) {
                money -= 10
            } else {
                money -= num
            }
        },
        getMoney() {
            return money
        }
    }
}
var myWallet = createWallet(99)
myWallet.add(1)
myWallet.deduct(100)
console.log(myWallet.getMoney)
  • 再練習一次
function createCounter() {
    var count = 0
    function addCount() {
        count ++
        return count
    }
   return addCount
}
var counter = createCounter()
console.log(counter())
console.log(counter())
// 不想讓外部存取到變數,外面看不到裡面的變數







你可能感興趣的文章

C# 用ini檔讀寫暫存資料

C# 用ini檔讀寫暫存資料

JS input 事件介紹

JS input 事件介紹

 [ 學習筆記系列 ] 網頁本質 (三) - JavaScript 篇

[ 學習筆記系列 ] 網頁本質 (三) - JavaScript 篇






留言討論