JavaScript 閉包與作用域問題解題解析:如何正確輸出 1 2 3
更新日期: 2024 年 11 月 4 日
在 JavaScript 中,for
迴圈與 setTimeout
的組合,經常會引發閉包與作用域的問題,導致結果與預期不符。
這篇文章將探討這種情境,並介紹兩種解決方法,使得以下程式碼能夠正確輸出 1 2 3
。
問題說明(在不能將 var
修改成 let
的條件下)
以下代碼的目標是讓程式輸出 1 2 3
:
for (var i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i);
}, 0)
}
console.log(i);
在解決這個問題時,我們必須在不更動 var
宣告的情況下達到正確輸出。
通常,在 JavaScript 中使用 let
來宣告變數可以限制其作用域至每次迴圈內部,但這裡不得更動 var
。
因此,需要其他方式,使每次迴圈中的 i
值在異步操作時不受後續迴圈影響。
當這段代碼執行時,console.log(i)
的結果並非 1 2 3
,而是 3
3
3
原因在於,for
迴圈中的 var i
是一個全域變數,每次迴圈中並未將 i
綁定到該次迴圈。
因此 setTimeout
在迴圈結束後才執行,最終輸出重複的值。
解決思路 1:使用 bind
綁定變數
解決方案
為了解決這個問題,可以使用 bind
方法,將當前的 i
值綁定到一個新的函數中,確保 i
值在每次迴圈中獨立存在,並在異步操作執行時仍然保持該次迴圈的 i
值。
bind
方法的觀念
在 JavaScript 中,bind
方法會創建一個新的函數,並將特定的參數與函數綁定。
此方法可以用來綁定當前 i
值,保證每次迴圈中的 i
值獨立,避免受後續變數影響。
實作代碼
以下是使用 bind
方法綁定變數的程式碼:
for (var i = 0; i < 3; i++) {
const aa = ((n) => {
console.log(n);
}).bind(null, i);
setTimeout(aa, 0);
}
在這段程式碼中,我們將當前的 i
值作為參數傳遞給 bind
方法,創建了一個新的函數 aa
,並綁定當前的 i
值給 n
,使其在 setTimeout
執行時能輸出獨立的 i
值。
運行結果
透過 bind
,我們為每次迴圈的 i
值創建了一個新的綁定,因此最終輸出將會是 1 2 3
,因為每次執行 setTimeout
時,使用的是當前迴圈的 i
值副本。
解決思路 2:使用 IIFE 綁定變數
解決方案
另一種解法是使用立即執行函數 (IIFE),為每次迴圈建立一個新的作用域,使得 i
值在每次執行時獨立存在,確保 i
不會受到後續迴圈的影響。
IIFE 的概念
立即執行函數 (IIFE, Immediately Invoked Function Expression) 是一種 JavaScript 表達式,可以在定義後立即執行。
IIFE 能夠建立新的作用域,在處理異步操作時可以捕捉當前值,避免後續變數的影響。
實作代碼
以下是利用 IIFE 綁定變數的程式碼:
for (var i = 0; i < 3; i++) {
setTimeout(((n) => {
console.log(n);
})(i), 0);
}
在此解法中,我們使用 IIFE 在每次迴圈中立即執行並傳入當前的 i
值。
這樣 n
值在 IIFE 作用域內保持不變,確保每次執行的 setTimeout
引用的是當前 i
的值。
運行結果
此方式通過 IIFE 捕捉每次迴圈的 i
值,並作為局部變數 n
使用,最終結果同樣是 1 2 3
,因為 setTimeout
中引用的是當時 n
的獨立值。
結論
透過使用 bind
或 IIFE,能夠在不修改 var
為 let
的情況下正確捕捉每次迴圈的變數 i
值,解決 JavaScript 中常見的閉包問題。
理解並掌握這些解決方法,將有助於在 JavaScript 中更有效地控制變數作用域。