當 Callback 使用 for Loop 中 var 所設定的 Counter 時,一不小心就會出乎我們預期。
Version
macOS Catalina 10.15
VS Code 1.38.1
Quokka 1.0.254
ECMAScript 5
ECMAScript 2015
For Loop
let fn = () => {
let result = [];
for(var i = 0; i < 3; i++) {
result.push(i);
}
return result;
};
fn(); // ?
若想將 primitive 放進 array,直接使用 for loop 與 Array.prototype.push() 即可。

Callback in Loop
let fn = () => {
let result = [];
for(var i = 0; i < 3; i++) {
result.push(() => i);
}
return result;
};
fn()[0](); // ?
fn()[1](); // ?
fn()[2](); // ?
若想將 function 放進 array,然後再依序執行 array 中的 function。
原本我們預期 i 為定義時的 i,而非執行完時的 i,所以應該印出 0、1 與 2,但卻都全部印出 3。
原因在於我們只是將 () => i 放進 array 中並未執行,然後將整個 array 回傳。
當 [0]() 對 array 中的 () => i 執行時,由於本身 local scope 沒有 i,因此透過 lexical scope 找到 fn() 的 i,此時 i 已經是執行完的 3。

IIFE
let fn = () => {
let result = [];
for(var i = 0; i < 3; i++) {
(i => {
result.push(() => i);
})(i);
}
return result;
};
fn()[0](); // ?
fn()[1](); // ?
fn()[2](); // ?
所以我們必須 clousure 將 i 鎖起來,避免拿到執行完的 i。
透過 IIFE 將 result.push() 包起來,如此 i 透過 anonymous function 傳進來後,closure 會產生新的 function scope,如此 callback 透過 lexical scope 抓到 i 時,就是定義時的 i,而非執行完的 i。

Let
let fn = () => {
let result = [];
for(let i = 0; i < 3; i++) {
result.push(() => i);
}
return result;
};
fn()[0](); // ?
fn()[1](); // ?
fn()[2](); // ?
若使用 ES6 就有更直覺的寫法了,因為 let 使每個 {} 都有自己的 block scope,因此等效於 ES5 使用 IIFE 與 closure。

Array.prototype.map
let fn = () => [0, 1, 2].map(i => () => i);
fn()[0](); // ?
fn()[1](); // ?
fn()[2](); // ?
若使用 Array.prototype 下的 method 使用 callback 也沒問題,因為每個 () => i 都有新的 block scope, 所以 i 都是定義時的 i,而非執行完後的 i。

Conclusion
- 若要在
for loop中使用 Closure,建議改用 ECMAScript 2015 的let - 改用
Array.prototype下的 method 也能避免 callback 抓到執行完後的變數