Js 一道閉包問題

Cyang發表於2016-12-16

問題描述

前幾天去找前端的實習生工作,被一道 js 的閉包題目給卡住了,題目大致如下:

for (var i = 0; i < 10; i++) {
  setTimeout(function() {
    console.log(i);
    }, 0);
}

稍微瞭解 js 的非同步機制的都知道,輸出結果是: 10 10 10 ... 10

但是面試官又問我:其實希望得到的是0 1 2 ... 9,如何能夠解決這個問題?我回答不上來。

思考分析

我一直從非同步的角度在思考,最終的結論也就是:這裡可以不用非同步函式……

然而這是個閉包問題,面試官給我的答案是使用立即執行函式(IIFE)
我一下子明白過來,在《JavaScript高階程式設計》裡就有收錄這個問題。(Update: 第三版 7.2.1)

我發現,其實這個問題除了和閉包有關係之外,也和var變數與其他語言(比如java)不同的作用域有關係。
通過手動的方式寫這個迴圈可以發現這些問題:

{
  var i = 0;
  setTimeout(function() { console.log(i); }, 0);
}
{
  var i = 1;
  setTimeout(function() { console.log(i); }, 0);
}
...
{
  var i = 9;
  setTimeout(function() { console.log(i); }, 0);
}
{
 var i = 10; 
 // Update : i 要到達 10 才會不滿足語塊的執行條件,之前沒有注意,誤導了大家,不好意思
}

所以for執行的每一步都是給i重新賦值和往事件佇列推個事件。
由於for的語塊不是var變數的作用域範圍,在事件開始執行時,所有事件的回撥函式通過閉包拿到的i是全域性作用域下的同一個i

解決方法

1. 使用立即執行函式

for (var i=0; i < 10; i++) {
  (function (temp) {
    setTimeout(function() {
      console.log(temp);
    }, 0);
  })(i);
}

通過立即執行函式,回撥函式閉包獲得的不是原來的i,而是立即執行函式的引數,這個引數剛好是i的拷貝,閉包獲得的拷貝並不指向記憶體中的同一物件,程式碼執行起來大概是這個樣子:

{
  var i = 0;
  var temp0 = i;
  setTimeout(function() { console.log(temp0); }, 0);
}
{
  var i = 1;
  var temp1 = i;
  setTimeout(function() { console.log(temp1); }, 0);
}
...
{
  var i = 9;
  var temp9 = i;
  setTimeout(function() { console.log(temp9); }, 0);
}

2. 使用 ES6 的 let 識別符號

for (let i = 0; i < 10; i++ ) {
  setTimeout(function() {
    console.log(i);
  }, 0);
}

是的,使用let的話就不會有這個閉包問題。
為什麼呢?
原來,在ES6中,為了修正var奇怪的函式作用域,新增了let,它的作用域是語塊:

{
  let a = true;
}
console.log(a); // undefined

現在再想象一下迴圈:

{
  let i = 0;
  setTimeout(function() { console.log(i); }, 0);
}
{
  let i = 1;
  setTimeout(function() { console.log(i); }, 0);
}
...
{
  let i = 9;
  setTimeout(function() { console.log(i); }, 0);
}

回撥函式閉包獲取的i不再是同一個了!

相關文章