for-timeout

最近在网上看到一篇关于for循环的面试题,可以让自己回顾一下作用域的基础知识。

不多说,题目如下:

下面的代码输出的是什么

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

相信稍微了解过一些作用域相关知识的,都可以很快回答出,会打印出5个5,而且是在1s后同时打印出来。

其实这段代码相当于

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

关键在于var创建了一个全局的变量,而不是一个for循环内的块作用域内的变量。

那么我们要怎么修改让输出改为每隔1s输出,且输出0-4呢。

既然块作用域解决不了我们的问题,那么我们可以创建一个函数作用域来保存我们的变量,创建一个函数作用域,很明显需要一个函数

1
2
3
4
5
6
7
8
9
function timeOutPrint(i) {
setTimeout(function(){
console.log(i)
}, 1000 * i)
}

for (var i = 0; i < 5; i++) {
timeOutPrint(i);
}

当然我们也可以使用IIFE

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

不过从代码的角度来说上面将函数封装起来可以更加便于阅读

我们换一个角度去看这个问题,其实我们是出现了一个异步的代码,那么处理异步的其中一个方法就是使用promise。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function timeOutPrint(i) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(i)
resolve();
}, i * 1000);
});
}

var tasks = [];
for (var i = 0; i < 5; i++) {
tasks.push(timeOutPrint(i));
}

Promise.all(tasks).then(()=>{
console.log('end');
})

我们换一个角度,其实当我们一开始写这段代码,心里其实是希望,for循环可以暂停,然后隔1s进入下一个循环,那么如果按照这个思路去改写呢

使用promise可以解决这个问题,但是因为js的事件循环,会导致Promise.resolve也会在for循环之后执行,所以不可以直接传入i,这个用了resolve传值得办法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function timeOutPrint(i) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(i)
resolve(i);
}, 1000);
});
}

var state = Promise.resolve(-1);
for (var i = 0; i < 5; i++) {
state = state.then((value) => {
return timeOutPrint(++value);
})
}

其实如果使用let就可以有比较好的效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function timeOutPrint(i) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(i)
resolve(i);
}, 1000);
});
}

var state = Promise.resolve(true);
for (var i = 0; i < 5; i++) {
let j = i;
state = state.then(() => {
return timeOutPrint(j);
})
}

这里我们看到了let的使用,后面我们会再次用到这个ES6的语法。

我们通过生成一系列顺序执行的Promise来实现,但是并没有真正的使for循环中断,而使代码中断的语法则可以使用ES6中的Generatoryield

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
function isPromise(obj) {
return 'function' == typeof obj.then;
}

var loop = function(gen) {
var g = gen();

var run = function() {
console.log(g);
var r = g.next();
if(r.done) {
return;
}
if(isPromise(r.value)) {
r.value.then(()=> {
run(g);
})
} else {
run(g);
}
};
run();
}

function timeOutPrint(i) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(i)
resolve(i);
}, 1000);
});
}

function *main() {
for (var i = 0; i < 5; i++) {
yield timeOutPrint(i);
}
}

loop(main);

这里我们写了一个简单的Generator的执行函数loop

Generator可以很好的实现代码暂停的功能,但是不可以执行像普通函数一样fn(),而是需要搭配一个执行函数来执行.next()方法。

那么是否有更好的方案,asyncawait出现了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function timeOutPrint(i) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(i)
resolve(i);
}, 1000);
});
}
async function main() {
for (var i = 0; i < 5; i++) {
await timeOutPrint(i);
}
}
main();

或者

1
2
3
4
5
6
7
8
9
10
11
12
var sleep = (time) => new Promise((resolve) => {
setTimeout(resolve, time);
})

async function main() {
for (var i = 0; i < 5; i++) {
await sleep(1000);
console.log(i);
}
}

main();

还记得上面我们用过的let

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

说明可以参看块级作用域和let