對閉包,我一直都有誤解

非梧不棲發表於2018-07-04

1 從一個尷尬的例子說起

<ul class="wrap">
  <li>1</li>
  <li>2</li>
  <li>3</li>
</ul>

<script>
  var addEvent = function (nodes) {
    for (var i = 0; i < nodes.length; i++) {
      nodes[i].onclick = function () {
        console.log(i)
      }
    }
  }

  var wrap = document.querySelectorAll('.wrap > li')
  addEvent(wrap)
</script>
複製程式碼
  • 結果如大家所想,點選每個li,都會列印3

addEvent函式的本意,是想傳遞給每個事件處理函式:一個唯一的 i 。
而沒有按照預期的原因是:事件處理函式繫結的是變數 i 本身,而不是函式在構造時的變數 i 的值。

2 解決

2.1 最常規的解決

利用的是物件屬性不變性。

var addEvent = function (nodes) {
    for (var i = 0; i < nodes.length; i++) {
      nodes[i].index = i
      nodes[i].onclick = function () {
        console.log(this.index)
      }
    }
 }
複製程式碼

2.2 es6

利用的是let宣告的變數,僅在塊級作用域有效。
i 只在本輪迴圈有效。所以每次迴圈的 i 都是一個新的變數。JavaScript引擎內部會記住上一輪迴圈的值,作為初始化本輪變數 i 的基礎。


而對於var,因為宣告的變數是全域性的,也就是說,使用的是同一個變數。最終在列印輸出時,使用的就是全域性變數 i ,所以點選時,都是 3。

let addEvent = function (nodes) {
    for (let i = 0; i < nodes.length; i++) {
      nodes[i].onclick = function () {
        console.log(i)
      }
    }
}
複製程式碼

2.3 閉包(終於到了。。。)

如下所示,事件處理函式中,又返回了一個輔助函式。

  • 因為閉包的特性,
    • 該輔助函式,可以訪問他被建立時所處的上下文環境(可以簡單理解為:外部函式作用域。因為函式中宣告的變數或是傳遞給函式的變數,是存放在執行上下文中,而執行上下文又掛靠在自己的作用域中。)
    • 該輔助函式,訪問的是外部函式中的實際變數,而不是複製後的值。

所以,每次傳遞給handlers函式的引數 i 都會放到handlers的執行上下文中, 而每次呼叫函式,都會建立一個執行上下文物件,因為,互不干擾。

var addEvent = function (nodes) {
    var handlers = function (i) {
      return function () {
        console.log(i)
      }
    }
    for (var i = 0; i < nodes.length; i++) {
      nodes[i].onclick = handlers(i)
    }
}
複製程式碼

3 其他問題

3.1 關於for迴圈,使用letvar還有區別:

  • let,設定迴圈變數的那部分,是父級作用域;而迴圈體內部是單獨的子作用域

如下所示,會連續輸出3個 a 。首先let是不能重複宣告的,但該例中並沒有報錯。因為第一次迴圈結束,i++時,i 依舊是 0

for (let i = 0; i < 3; i++) {
  let i = 'a'
  console.log(i)
}
// a
// a
// a
複製程式碼
  • var,因為通過var宣告的是全域性變數,並且可以重複宣告,所以當 i++ 時,變為了 a++, 則第二次迴圈時,i 為 NaN,就結束了迴圈。
for (var i = 0; i < 3; i++) {
  var i = 'a'
  console.log(i)
}
// a
複製程式碼

3.2 建立函式的問題

  • 要避免在迴圈中建立函式。

回來最開始的地方,
為了給每個節點都繫結了事件處理函式,在迴圈中進行繫結。
這樣,每次都會建立一個新的匿名函式,帶來的只有無味的計算,還容易引起混淆,正如這個例子所示。

var addEvent = function (nodes) {
    for (var i = 0; i < nodes.length; i++) {
      nodes[i].onclick = function () {
        console.log(i)
      }
    }
}
複製程式碼

相關文章