前端面试中警察会碰到下面这样类型的题,很多前端工程师并不能完全回答正确,这是一道怎样的一道神奇的面试题呢? 题目如下:下面这段代码的输出实际运行效果如何,new Date输出时间差是多少?
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);
正确结果是:立即输出5,然后大概1秒后,连续输出四个5。 如果我们约定,用箭头表示其前后的两次输出之间有 1 秒的时间间隔,而逗号表示其前后的两次输出之间的时间间隔可以忽略 运行结果可以描述为:5 -> 5,5,5,5,5
这里的问题主要出现在,js中 var 定义的 i 是函数级作用域,函数块循环中并不会保留每次i的值的复制,而是共享一个i,等到setTimeout中的回调函数执行时i已经加到5了。 并且还考察了JS 中的定时器工作机制,setTimeout中回调事件的处理是提交到事件队列中,等到延时时间到就都依次执行了,这5个回调都等待了1s,故1s后相继输出而几乎没有时间间隔。
那么如何实现:0 -> 1 -> 2 -> 3 -> 4 -> 5 的输出呢? 下面总结出了一些实践方案。
利用闭包 熟悉闭包思想的话,传递i的值作为函数参数,并立即执行,则会复制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 * i);
};
for (var i = 0 ; i < 5 ; i++) {
output(i);
}
output(i);
利用bind 上面利用闭包就是利用了私有作用域不能立即被销毁,导致了 i 的保留,没有共享一个 i,同理,用 bind 方法也会保留 i 的引用,不会共享一个 i。
1
2
3
4
5
6
7
8
for (var i = 0 ; i < 5 ; i++) {
setTimeout(function (j ) {
console .log(new Date , j);
}.bind(this , i), 1000 * i);
}
setTimeout(function ( ) {
console .log(new Date , i);
}, i * 1000 );
1
2
3
4
5
6
for (var i = 0 ; i < 5 ; i++) {
setTimeout(console .log.bind(console , new Date , i), 1000 * i);
}
setTimeout(function ( ) {
console .log(new Date , i);
}, i * 1000 );
但是这里的上一个 new Date 是固定值,不符合预期要求,只是提供下简洁代码的思路。
利用setTimeout第三个参数 很多人容易忽略定时器可以传递三个及以上的参数 (从第三个参数开始是传入函数里面的参数),这个参数传递恰好可以解决i的作用域问题
1
2
3
4
5
6
7
8
for (var i = 0 ; i < 5 ; i++) {
setTimeout(function (j ) {
console .log(new Date , j);
}, 1000 * i, i);
}
setTimeout(function ( ) {
console .log(new Date , i);
}, i * 1000 );
利用ES6的let
1
2
3
4
5
6
7
8
9
10
for (let i = 0 ; i < 5 ; i++) {
setTimeout(function ( ) {
console .log(new Date , i);
}, 1000 * i);
}
setTimeout(function ( ) {
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 = [];
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));
}
Promise .all(tasks).then(() => {
setTimeout(() => {
console .log(new Date , i);
}, 1000 );
});
利用ES7的async, await
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const sleep = (timeountMS ) => new Promise ((resolve ) => {
setTimeout(resolve, timeountMS);
});
(async ( ) => {
for (var i = 0 ; i < 5 ; i++) {
await sleep(1000 );
console .log(new Date , i);
}
await sleep(1000 );
console .log(new Date , i);
})();
改善版:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function timeout (n ) {
return new Promise (resolve => setTimeout(resolve, n))
}
async function output (n ) {
let i
for (i = 0 ; i < n; i++) {
console .log(i)
await timeout(1000 )
}
return i
}
output(5 ).then(console .log)