點燈坊

失くすものさえない今が強くなるチャンスよ

如何避免 Callback 在迴圈中不如預期 ?

Sam Xiao's Avatar 2019-10-09

當 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() 即可。

loop004

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,所以應該印出 012,但卻都全部印出 3

原因在於我們只是將 () => i 放進 array 中並未執行,然後將整個 array 回傳。

[0]() 對 array 中的 () => i 執行時,由於本身 local scope 沒有 i,因此透過 lexical scope 找到 fn()i,此時 i 已經是執行完的 3

loop000

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

loop001

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。

loop002

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

loop003

Conclusion

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