1. 程式人生 > >迴圈與閉包 之 for迴圈經典問題解釋 / 結合《你不知道的JS》與《高程》案例

迴圈與閉包 之 for迴圈經典問題解釋 / 結合《你不知道的JS》與《高程》案例

案例一

for (var i = 1; i <= 5; i++) {
  setTimeout( function timer() {
    console.log(i);
  }, i*1000)
}

輸出結果:

  • 當時間是固定的數,如0、1000、6000,執行結果就是0、1、6秒後,一次輸出五個6;
  • 當時間是 i*1000, 輸出是:每隔1秒,輸出一個6,共5次。

程式碼中到底有什麼‘缺陷’,導致它的行為與 語義暗示的不一致呢?

缺陷 是 我們試圖假設
迴圈中的每一個迭代在執行時,都會給自己’捕獲’一個i的副本。但是,根據作用域的工作原理,實際情況是,儘管迴圈中的五個函式是在各個迭代中分別定義的,但是它們都被封閉在一個共享的全域性作用域中,因此實際上只有一個 i。

這樣說的話,當然所以函式共享一個 i 的引用。
迴圈結構讓我們誤以為背後還有更復雜的機制在起作用,實際上並沒有。如果將延遲函式的回撥重複定義5次,完全不使用迴圈,那它同這段程式碼是完全等價的。

                             ----《你不知道的JS 上卷》p49

show me the code

第一種情況

setTimeout()第二個引數是個常數

書中那段話就是說,

for (var i = 1; i <= 5; i++) {
  setTimeout( function timer() {
    console.log(i);
  }, 1000
) }

等價於

// 注:執行棧內迴圈需要先結束

var i = 1;
// 定時器將其回撥函式加入任務列表,執行棧清空一秒後執行
var i = 2;
// 定時器將其回撥函式加入任務列表,執行棧清空一秒後執行
var i = 3;
// 定時器將其回撥函式加入任務列表,執行棧清空一秒後執行
var i = 4;
// 定時器將其回撥函式加入任務列表,執行棧清空一秒後執行
var i = 5;
// 定時器將其回撥函式加入任務列表,執行棧清空一秒後執行
var i = 6;
// 定時器將其回撥函式加入任務列表,執行棧清空一秒後執行

// 迴圈結束後,執行緒讀取任務列表,將定時事件對應的非同步任務(回撥函式)入棧執行
console.log(i); console.log(i); console.log(i); console.log(i); console.log(i); console.log(i);

輸出情況:
當時間是固定的數,如0、1000、6000,執行結果就是0、1、6秒後,一次輸出五個6;

等到這些回撥執行時,i的值就是6

第二種情況

setTimeout()第二個引數含有變數

同上

for (var i = 1; i <= 5; i++) {
  setTimeout( function timer() {
    console.log(i);
  }, i*1000)
}

等價於


var i = 1;
// 定時器將其回撥函式加入任務列表,執行棧清空一秒後執行
var i = 2;
// 定時器將其回撥函式加入任務列表,執行棧清空二秒後執行
var i = 3;
// 定時器將其回撥函式加入任務列表,執行棧清空三秒後執行
var i = 4;
// 定時器將其回撥函式加入任務列表,執行棧清空四秒後執行
var i = 5;
// 定時器將其回撥函式加入任務列表,執行棧清空五秒後執行
var i = 6;
// 定時器將其回撥函式加入任務列表,執行棧清空六秒後執行

// 迴圈結束後,執行緒讀取任務列表,將定時事件對應的非同步任務(回撥函式)入棧執行
console.log(i);

console.log(i);

console.log(i);

console.log(i);

console.log(i);

console.log(i);

輸出情況:
當時間是 i*1000, 輸出是:每隔1秒,輸出一個6,共5次。

因為等到這些回撥執行時,i的值就是6

HOW TO FIX IT

我知道,閉包!

for (var i = 1; i <= 5; i++) {
  (function() {
    setTimeout( function timer() {
      console.log(i);
    }, i*1000)
  })()
}

然並卵,還是一秒一個6,五次

因為,新加上的 IIFE(建立並立即執行) 作用域是”空的”,它並沒有自己的變數。執行棧清空後,執行緒從任務佇列裡讀取回調函式,它們還是引用那個唯一的全域性變數i。

正確的閉包姿勢:

通過在閉包作用域中新增自己的變數,從而在每次迭代中,捕獲i的副本。

for (var i = 1; i <= 5; i++) {
  (function() {
    var j = i
    setTimeout( function timer() {
      console.log(j);
    }, j*1000)         //至於時間這裡,是i 是j無所謂
  })()
}

更簡潔的姿勢:

for (var i = 1; i <= 5; i++) {
  (function(j) {
    setTimeout( function timer() {
      console.log(j);
    }, j*1000)
  })(i) 
}

由此,能夠輸出:1 2 3 4 5,一秒一個

ES6的開啟方式: 塊作用域

for (var i = 1; i <= 5; i++) {

    let j = i
    setTimeout( function timer() {
      console.log(j);
    }, j*1000)

}

或直接在for迴圈頭部裡,每次迭代都宣告一次

for (let i = 1; i <= 5; i++) {

    setTimeout( function timer() {
      console.log(i);
    }, i*1000)

}

案例2

《JavaScript高階程式設計第三版》 p181

var a = function ceateFunctions() {
  var result = new Array();

  for (var i = 0; i < 10; i++) {
    result[i] = function () {
      return i
    }
  }

  return result
}
console.log(a()[0]());
//0 - 9 輸出都是10  

道理一樣。

都是因為引用同一個i,且沒能在迭代中捕獲i的副本,或者沒能在迭代中及時按當時的值執行。直到i早都變成10了,才執行,RHS引用的結果當然是i此刻的值,即10。

閉包處理:

var a = function ceateFunctions() {
  var result = new Array();

  for (var i = 0; i < 10; i++) {
  // 建立匿名函式,並立即執行之,將執行結果賦值給陣列
    result[i] = (function(num) {
      return function() {
        return num;
      };
    })(i);
  }

  return result;
};
console.log(a()[0]());