迴圈與閉包 之 for迴圈經典問題解釋 / 結合《你不知道的JS》與《高程》案例
阿新 • • 發佈:2019-01-09
案例一
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]());