JavaScript 有一個特性可能會給開發者帶來一些困擾,那就是與循環和作用域相關的問題。

讓我們來看一個例子:

const operations = []

for (var i = 0; i < 5; i++) {
 operations.push(() => {
 console.log(i)
 })
}

for (const operation of operations) {
 operation()
}

這段代碼遍歷了 5 次,每次將一個函數添加到名為 operations 的數組中。這個函數會將循環索引變量 i 輸出到控制台。

之後這些函數會被執行。

預期結果應該是:

0
1
2
3
4

但實際上卻是這樣的:

5
5
5
5
5

為什麼會這樣呢?這是因為使用了 var

由於 var 声明是提升的,上面的代碼等價於:

var i;
const operations = []

for (i = 0; i < 5; i++) {
 operations.push(() => {
 console.log(i)
 })
}

for (const operation of operations) {
 operation()
}

因此,在 for-of 循環中,i 仍然是可見的,它的值等於 5,函數中對 i 的任何引用都將使用這個值。

那麼,我們應該如何使事情按我們的意願運行呢?

最簡單的解決方案是使用 let 声明。在 ES6 中引入的 let 声明可以很好地避免 var 声明的一些怪異問題。

只需將循環變量中的 var 改為 let 就能正常工作:

const operations = []

for (let i = 0; i < 5; i++) {
 operations.push(() => {
 console.log(i)
 })
}

for (const operation of operations) {
 operation()
}

下面是輸出結果:

0
1
2
3
4

為什麼會這樣呢?這是因為在每次循環迭代中,i 都會被創建為一個新的變量,並且添加到 operations 數組的每個函數都有自己的 i 副本。

請記住,你不能在這種情況下使用 const,因為在第二次迭代中,for 嘗試將新值賦給它會引發錯誤。

解決這個問題的另一種方式在 ES6 前的代碼中很常見,被稱為立即執行函數表達式(IIFE)。

在這種情況下,你可以將整個函數包裹起來,並將 i 綁定到它上面。由於以這種方式創建的函數立即被執行,你從中返回了一個新的函數,所以稍後可以執行它:

const operations = []

for (var i = 0; i < 5; i++) {
 operations.push(((j) => {
 return () => console.log(j)
 })(i))
}

for (const operation of operations) {
 operation()
}