- 首先一開始是下面這樣子的:
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(new Date, i);
}, 1000);
}
console.log(new Date, i);
上面的代碼等同于:
for (var i = 0; i < 5; i++) {
function xxx() {
console.log(new Date, i);
}
setTimeout(xxx, 1000);
} - 注意:setTimeout里面傳入的第一個匿名函數,等價于在setTimeout語句外面定義的一個函數。所以它的閉包范圍是變量i所在的作用域,所以可以訪問到i.
- 解析:大家都知道 i 是外面的變量,所有的setTimeout里面的函數都會指向同一個 i ,所以最終輸出:
Sat Mar 25 2017 12:40:11 GMT+0800 (中國標準時間) 5
undefined
VM87:3 Sat Mar 25 2017 12:40:12 GMT+0800 (中國標準時間) 5
VM87:3 Sat Mar 25 2017 12:40:12 GMT+0800 (中國標準時間) 5
VM87:3 Sat Mar 25 2017 12:40:12 GMT+0800 (中國標準時間) 5
VM87:3 Sat Mar 25 2017 12:40:12 GMT+0800 (中國標準時間) 5
VM87:3 Sat Mar 25 2017 12:40:12 GMT+0800 (中國標準時間) 5
- 下面改造開始:要讓控制臺能夠輸出1234
-
可以用閉包:(每次把新的變量i傳進一個匿名的立即執行函數,每次 j
都能得到不同的 i ,因為j在匿名函數的作用域內,函數的執行作用域每執行一次都會重新生成,所以每次的 j 都不是同一個)
for (var i = 0; i < 5; i++) {
(function(j) { // j = i
setTimeout(function() {
console.log(new Date, j);
}, 1000);
})(i);
}console.log(new Date, i);
可以用傳參法:(其實是把閉包的匿名函數擴展出來)
function wrapper(j) {
// function fn() {
// console.log(new Date, j);
// }
// setTimeout(fn,1000);
// 上面全部的寫法,等價于:
setTimeout(function () {
console.log(new Date, j);
},1000);
}
for (var i = 0; i < 5; i++) {
wrapper(i);
}
console.log(new Date, i);
輸出如下:
Sat Mar 25 2017 13:20:57 GMT+0800 (中國標準時間) 5
undefined
VM94:8 Sat Mar 25 2017 13:20:58 GMT+0800 (中國標準時間) 0
VM94:8 Sat Mar 25 2017 13:20:58 GMT+0800 (中國標準時間) 1
VM94:8 Sat Mar 25 2017 13:20:58 GMT+0800 (中國標準時間) 2
VM94:8 Sat Mar 25 2017 13:20:58 GMT+0800 (中國標準時間) 3
VM94:8 Sat Mar 25 2017 13:20:58 GMT+0800 (中國標準時間) 4
注意:這里有一個容易犯錯的點,setTimeout的第一個參數要求的是一個函數的引用,而不是執行一個函數,所以只能傳入函數名xxx的形式,而不能傳入xxx(a,b),這也意味著不能在xxx上直接傳參。
- 所以,要傳參的話,
只能在setTimeout外面再包裹一層函數,然后定制編寫xxx函數。
再補充一個重要知識點,如果一定要在xxx中傳參,又不想用閉包,可以使用setTimeout的第3個參數,從第3個參數往后的參數,都會傳入xxx里作為形參使用。
- 例如:上面的代碼也可以寫成:
setTimeout(function(j) {
console.log(new Date, j); //可以輸出01234
}, 1000 ,i); - 如果硬是在setTimeout()中傳入的xxx()的形式,那么只會以正常任務的方式立即執行xxx(),而不會放入任務隊列里去,也就是定時器失效。
- 例如:
for (var i=0; i<5; i++){
function xxx(j) {
console.log(new Date,j)
}
setTimeout(xxx(i),1000);
}
console.log(new Date, i);
以上代碼的輸出如下:
Sat Mar 25 2017 13:37:27 GMT+0800 (中國標準時間) 0
VM2209:3 Sat Mar 25 2017 13:37:27 GMT+0800 (中國標準時間) 1
VM2209:3 Sat Mar 25 2017 13:37:27 GMT+0800 (中國標準時間) 2
VM2209:3 Sat Mar 25 2017 13:37:27 GMT+0800 (中國標準時間) 3
VM2209:3 Sat Mar 25 2017 13:37:27 GMT+0800 (中國標準時間) 4
VM2209:7 Sat Mar 25 2017 13:37:27 GMT+0800 (中國標準時間) 5
undefined
- 解析:可以看到,所有時間幾乎是同一時刻輸出的,而且是按012345的順序,最后退出函數返回undefined,也就是說在退出函數前,setTimeout里的xxx就已經執行了,并沒有進入任務隊列。由此可以說明,setTimeout的第一個參數期待的是函數名,而不是一個函數的執行。
- 當然,如果xxx()執行后返回的是一個函數,理論上也可以設置定時器函數,但傳參又出現問題,太過復雜,現實暫時沒有遇到必須這樣的問題,不作討論。
ES6及Promise登場
- 上述還可以用ES6及Promise來實現,具體如下:
var tasks=[];
function output(j) {
var promise = new Promise( function(resolve, reject) {
setTimeout(function () {
console.log(new Date(), j);
resolve(j);
// console.log("這是一個小補充喲"+j);
},1000 * i);
});
promise.then(function (j) {
// console.log("這是then里的一點小補充喲"+j);
});
return promise;
}
for (var i=0;i<5;i++){
tasks.push(output(i)); //執行順序:首先在這里將定時器設置好,
// 也就是循環設置定時器,由于執行時間很快,每次循環的間隔可以忽略不計,
// 所以可以認為是設置了5個時間分別為0~4s的定時器,已經開始計時。
// 計時后返回promise對象,放在tasks數組中
}
// 最后,在這里相當于是一個總的監聽器,當前面4個任務都resolve以后,執行最后一個設置定時器任務,
// 到時間以后,執行輸出5
// 關于執行順序,前4個task是靠定時器的時間差別來決定先后輸出順序的,最后一個5的task,是依靠異步回調來執行的。
Promise.all(tasks).then(function () {
setTimeout(function () {
console.log(new Date(), i);
},1000);
}); - 解析:// 關于任務隊列:
// 首先第一趟主任務隊列走下來,執行了設置定時任務,將promise對象放入tasks數組,并設置好then回調的工作。
// 然后第二趟,執行定時任務隊列,運行consolo.log語句,
// 然后遇到resolve,需要調用相應的then里面的回調語句(如果有的話)。
// 但是注意,這里調用then的時機,是在本次任務的主代碼執行完畢后,
// 也就是說,如果setTimeout語句中的resolve()后面還有執行語句,要先執行那些語句,最后才執行resolve對應的then回調。 - 要確定resolve相應的回調語句的執行順序,可以看下面的輸出結果(將代碼里的兩句console.log去掉注釋即可):
Sat Apr 01 2017 13:15:47 GMT+0800 (中國標準時間) 0
(index):41 這是一個小補充喲0
(index):45 這是then里的一點小補充喲0
(index):39 Sat Apr 01 2017 13:15:48 GMT+0800 (中國標準時間) 1
(index):41 這是一個小補充喲1
(index):45 這是then里的一點小補充喲1
(index):39 Sat Apr 01 2017 13:15:49 GMT+0800 (中國標準時間) 2
(index):41 這是一個小補充喲2
(index):45 這是then里的一點小補充喲2
(index):39 Sat Apr 01 2017 13:15:50 GMT+0800 (中國標準時間) 3
(index):41 這是一個小補充喲3
(index):45 這是then里的一點小補充喲3
(index):39 Sat Apr 01 2017 13:15:51 GMT+0800 (中國標準時間) 4
(index):41 這是一個小補充喲4
(index):45 這是then里的一點小補充喲4
(index):60 Sat Apr 01 2017 13:15:52 GMT+0800 (中國標準時間) 5
最后強調一遍,resolve的回調函數是在本輪“事件循環”結束時執行,setTimeout(fn, 0)在下一輪“事件循環”開始時執行。
-
如果覺得上面的例子太復雜,看下面:
setTimeout(function () {
console.log('three');
}, 0);Promise.resolve().then(function () { console.log('two'); }); console.log('one'); // one // two // three