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()
}