JavaScript 同步異步、變量作用域、閉包 簡單應用

熟悉同步和異步的區別、變量作用域、閉包(Closure)等概念的理解

一般

1
2
3
4
5
6
7
for (var i = 0; i < 5; i++) { 
setTimeout(function() {
console.log(new Date, i);
}, 1000);
}

console.log(new Date, i);

輸出結果

2017-03-18T00:43:45.873Z 5 (最後一段的console.log)
2017-03-18T00:43:46.866Z 5 (從這開始都是 setTimeout 的 console.log)
2017-03-18T00:43:46.868Z 5
2017-03-18T00:43:46.868Z 5
2017-03-18T00:43:46.868Z 5
2017-03-18T00:43:46.868Z 5
2017-03-18T00:43:46.868Z 5

在執行這段程式碼時, for 迴圈會在一瞬間跑完, 並且執行最後一段的console.log, 至於 setTimeout 則是在一秒後啟動, setTimeout 啟動後由於 i 變數已經被加到5, 所以setTimeout Function 中 console.log 出來的結果也都會是 5。

最後一行的console.log 可以讀到變數 i 是因為var 的特性, 所以ES6 之後才衍生出了 let (請參閱阮一峰的ES6)

閉包

但上述的輸出結果並不是期望的輸出結果, 如果想要呈現的輸出方式是 5,0,1,2,3,4 要怎寫?

以下是用閉包的方式 + IIFE(Immediately Invoked Function Expression), 一開始學習JS的人會覺得不是很好懂, 需琢磨一陣子才能真正理解

1
2
3
4
5
6
7
8
9
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);

更符合直覺的作法, 對循環過程作手腳, 讓負責輸出的程式碼都能拿到每次循環的i值

1
2
3
4
5
6
7
8
9
10
11
var output = function (i) { 
setTimeout(function() {
console.log(new Date, i);
}, 1000);
};

for (var i = 0; i < 5; i++) {
output(i); // 這裡傳過去的 i 值被複製了
}

console.log(new Date, i);

ES6

如果要讓輸出結果是 0, 1, 2, 3, 4, 5
並且兩處循環的 console.log 不變, 精準的描述的話是代碼執行時, 立刻輸出 0,
之後每一秒輸出 1 2 3 4 5

暴力解法

1
2
3
4
5
6
7
8
9
10
11
for (var i = 0; i < 5; i++) { 
(function(j) {
setTimeout(function() {
console.log(new Date, j);
}, 1000 * j)); // 這裡修改 0~4 的定時器時間
})(i);
}

setTimeout(function() { // 這裡增加定時器,超時設置為 5 秒
console.log(new Date, i);
}, 1000 * i);

ES6 Promise

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const tasks = []; 

for (var i = 0; i < 5; i++) { // 這裡 i 的聲明不能改成 let,如果要改該怎麼做?
((j) => {
tasks.push(new Promise((resolve) => {
setTimeout(() => {
console.log(new Date, j);
resolve(); // 這裡一定要 resolve,否則程式不會按照預期 work
}, 1000 * j); // 定時器的超時時間逐步增加
}));
})(i);

}

Promise.all(tasks).then(() => {
setTimeout(() => {
console.log(new Date, i);
}, 1000); // 注意這裡只需要把超時設置為 1 秒
});

在更簡潔的程式碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const tasks = []; // 這裡存放異步操作的 Promise 

const output = (i) => new Promise((resolve) => {
setTimeout(() => {
console.log(new Date, i);
resolve();
}, 1000 * i);
});

// 生成全部的異步操作
for (var i = 0; i < 5; i++) {
tasks.push(output(i));
}

// 異步操作完成之後, 輸出最後的 i
Promise.all(tasks).then(() => {
setTimeout(() => {
console.log(new Date, i);
}, 1000);
});

ES7

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 模擬其他語言中的 sleep,實際上可以是任何異步操作 
const sleep = (timeountMS) => new Promise((resolve) => {
setTimeout(resolve, timeountMS);
});

(async () => { // 聲明即執行的 async 函數表達式
for (var i = 0; i < 5; i++) {
await sleep(1000);
console.log(new Date, i);
}

await sleep(1000);
console.log(new Date, i);
})();
0%