Closure 閉包


Posted by ericcch24 on 2020-10-16

什麼是閉包

參考資料:深入淺出瞭解 JavaScript 閉包(closure)

  • 當你看到一個 function 內 return 了另一個 function,通常就是有用到閉包的概念。
  • 在 function 內 return function,造成這種明明執行完畢卻還有東西被關住的現象,而這種情形就是一般人所熟知的閉包,Closure。
function test() {
  var a = 10
  function inner() {
    console.log(a) // 10
  }
  inner()
}

test()

----

function test() {
  var a = 10
  function inner() {
    console.log(a) 
    // inner() 可以記住上層的變數 10,
    // 類似 鎖在 function 內的感覺
  }
  return inner
}

var func = test() // func 就等於是 return 的 inner
func() // inner() , 還是存取得到 a 的值
function foo() {
  var a = 2;

  function bar() {
    console.log(a); // 這邊 a 會往上層找到 2
  }

  return bar;
}

var baz = foo(); // 設立一個變數來接 foo() 內部回傳的 bar

baz(); // 2
// 等於呼叫 bar()

說明:
在外面呼叫 baz() 也可以存取到 bar 的 scope,也就是可以存取到 foo scope 宣告的 a,也就是這個變數 a 就算被關住還是可以讓外面呼叫的 baz 存取到。

參考文章:你懂 JavaScript 嗎?#15 閉包(Closure)
文章中說明:

  • 函式能夠存取的範疇即是其被內嵌後往外推的範圍,例如 bar 被內嵌於 foo 之內,因此 bar 內變數可存取的範圍就是 foo 和全域範疇。因此,基於語彙範疇的變數查找規則,bar 內的 a 在自己的函式範疇內找不到定義的話,可往外層的範疇查找,於是在 foo 內找到了,得到 a 為 2。其中,console.log(a) 是執行 RHS 查找。
  • JavaScript 引擎的垃圾回收機制會釋放不再使用的記憶體,但閉包為了保留函式記得和存取其語彙範疇的能力,就會予以保留,不做記憶體回收。因此,bar 仍保留指向 foo 的內層範疇的參考,這個參考就是閉包。
  • 最後,雖然 baz 位於 bar 所定義的範疇之外,但由於閉包的緣故,bar 仍能正常執行,而得到 a 的值。

閉包的應用

function complex(num) {
  return num * num * num
}

function cache(func) {
  var ans = {}
  function inner(num) {
    if (ans[num]) {
      return ans[num]
    }

    ans[num] = func(num)
    return ans[num]
  }
  return inner
}

const cachedComplex = cache(complex)
// 需要設立一個變數來接 cache(func) 回傳的 function
// cachedComplex() 就等於執行 cache(func) 內的 inner(num)
// 且可以存取到上層的 var ans = {}

console.log(cachedComplex(20)) 
//  第一次執行有進入 ans[20] = complex(20),輸出 8000

console.log(cachedComplex(20)) 
// 因為第一次已經計算過 ans[20] = 8000,已記住
// 所以不需再跑一次計算的 complex(20),直接輸出 8000

console.log(cachedComplex(20)) 
// 同上 8000

閉包的優點

  • 把變數隱藏在裡面讓外部存取不到
  • ```javascript=
    var my_balance = 999
    function deduct(n) {
    my_balance -= (n > 10 ? 10 : n) // 超過 10 塊只扣 10 塊
    }

deduct(13) // 只被扣 10 塊
my_balance -= 999 // 還是被扣了 999 塊


變數還是暴露在外部,任何人都可以直接來改這個變數。這時我們利用閉包來改寫,

```javascript=
function getWallet() {
  var my_balance = 999
  return {
    deduct: function(n) {
      my_balance -= (n > 10 ? 10 : n) // 超過 10 塊只扣 10 塊
    }
  }
}

var wallet = getWallet()
wallet.deduct(13) // 只被扣 10 塊
my_balance -= 999 // Uncaught ReferenceError: my_balance is not defined

因為把餘額這個變數給藏在 function 裡面,所以外部是存取不到的,也就是沒辦法直接從外部修改 my_balance 的值,想要修改只能夠利用已經暴露出去的 deduct 這個函式,這樣子就達到了隱藏資訊的目的,確保這個變數不會隨意地被改到。


閉包常見陷阱

var btn = document.querySelectorAll('button')
for(var i=0; i<=4; i++) {
  btn[i].addEventListener('click', function() {
    alert(i)
  })
}

----
// 上面迴圈 i 的宣告就等於宣告在全域變數
var i
for(i = 0; i <= 4; i++) {
  btn[i].addEventListener('click', function() {
    alert(i)
  })
}

此時按按鈕都會跳出 5,怪怪
因為迴圈是長這樣

btn[0].addEventListener('click', function() {
  alert(i) // 這邊的 i 會存取到宣告在 global 的 i
})

btn[1].addEventListener('click', function() {
  alert(i)
})

...

本來就是幫它加一個 function 是按下去的時候會跳出 i 而已,並沒有直接執行這個 function。(這邊是 event loop 概念)

而每一個迴圈的 alert(i) 會往上層找到 global 的 i,
那這個 i 的值會是什麼?因為按按鈕的時候迴圈已經跑完了,所以這時候 global 的 i 早已變成 5(迴圈的最後一圈,i 加一變成 5,判斷不符合 i<=4 這個條件所以跳出迴圈),畫面也就跳出數字 5 了。

  • 解決方法 1:利用 closure 概念,加 function
function getAlert(num) {
  return function() {
    alert(num)
  }
}
for(var i=0; i<=4; i++) {
  btn[i].addEventListener('click', getAlert(i))
}

getAlert(i) 會「回傳」一個跳出 i 的 function,因此我額外產生了五個新的 function,每一個 function 裡面都有自己該跳出的值。

  • 解決方法 2 :用 let const
for(let i=0; i<=4; i++) {
  btn[i].addEventListener('click', function() {
    alert(i)
  })
}

因為 let 的特性,所以其實迴圈每跑一圈都會產生一個新的作用域,因此 alert 出來的值就會是你想要的那個值。
迴圈實際是這樣跑

{ // 塊級作用域
  let i=0
  btn[i].addEventListener('click', function() {
    alert(i) // 這邊的 i 就會存取到 block 內部宣告的 i
  })
}
{ // 塊級作用域
  let i=1
  btn[i].addEventListener('click', function() {
    alert(i)
  })
}
...

ECMAScript 中的作用域

以 ES3 為例

每個 EC (Execution Context)都有自己的 scope chain,當進入 EC 的時候 scope chain 會被建立。

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

也就是在進入 EC 的時候會做下面這件事:

scope chain = activation object + [[Scope]]

  • 什麼是 activation object(以下簡稱 AO)?

只有在進入「函式」的時候會產生這個 AO,而之後 AO 便被當作 VO 拿去使用。

什麼是 AO?可以把它直接當作 VO 的另外一種特別的型態,只在 function 的 EC 中出現,所以在 global 的時候我們有 VO,在 function 內的時候我們有 AO,但是做的事情都是一樣的,那就是會把一些相關的資訊放在裡面。

差別在哪裡?差別在於 AO 裡面會有一個arguments,其餘地方都是差不多的。


  • 什麼又是[[Scope]]

在建立 function 的時候會給一個 Scope,而這一個 Scope 會被設定到[[Scope]]去。

而在建立 function 時給的 Scope 就是當前 EC 的 Scope。


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

流程範例詳見參考資料:所有的函式都是閉包:談 JS 中的作用域與 Closure
額外補充:WTF is closure

var v1 = 10
function test() {
  var vTest = 20
  function inner() {
    console.log(v1, vTest) //10 20
  }
  return inner
}
var inner = test()
inner()
tags: Week16

#week16







Related Posts

Day 103

Day 103

每日心得筆記 2020-06-23(二)

每日心得筆記 2020-06-23(二)

關於 mount 生命週期

關於 mount 生命週期


Comments