當 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 抓到執行完後的變數