一道神奇的前端面试题

前端面试中警察会碰到下面这样类型的题,很多前端工程师并不能完全回答正确,这是一道怎样的一道神奇的面试题呢?
题目如下:下面这段代码的输出实际运行效果如何,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 的输出呢?
下面总结出了一些实践方案。

  1. 利用闭包
    熟悉闭包思想的话,传递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);
  1. 利用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 是固定值,不符合预期要求,只是提供下简洁代码的思路。

  1. 利用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);
  1. 利用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);
}
// let是块级作用域,这里会报错
setTimeout(function() {
console.log(new Date, i);
}, 1000 * i);
  1. 利用ES6的Promise
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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);
});
  1. 利用ES7的async, await
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);
})();

改善版:

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)