一道关于闭包面试题

概述

知乎上看到的题,自己再加了点东西,这年头前端面试题越来越繁杂了,曾经只是一道简单的闭包题而已。

第一版:循环

分析以下代码并给出运行结果。

1
2
3
for(var i = 0; i < 5; i ++) {
console.log(i);
}

结果很明显。

1
2
3
4
5
// 0
// 1
// 2
// 3
// 4

第二版:事件轮询和无块级作用域

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

JavaScript中使用var关键字定义的变量不存在块级作用域,因此等到主线程的当前任务(循环)执行完之后,setTimeout中的函数中的i引用的是同一个i,住循环执行结束后的i。因此结果是。

1
2
3
4
5
// 5
// 5
// 5
// 5
// 5

第三版:let/const和块级作用域

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

ES6中引入了新的关键字let和const,使用这两个关键字创建的变量都是有块级作用域的。

1
2
3
4
5
// 0
// 1
// 2
// 3
// 4

第四版:闭包

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

每一个闭包都有一个自己得函数作用域。

1
2
3
4
5
// 0
// 1
// 2
// 3
// 4

如果删除IIFE中的变量声明。

1
2
3
4
5
6
7
for(var i = 0; i < 5; i ++) {
(functino(){ // 注意这里删除了函数参数声明
setTimeout(function() {
console.log(i);
}, i * 1e3);
})(i);
}

则闭包内部没有对i持有引用,会按照作用域链回溯到外部的i,因此还是。

1
2
3
4
5
// 5
// 5
// 5
// 5
// 5

如果将闭包写在setTimeout内部。

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

IIFE会立即调用,实际上代码等价于。

1
2
3
for (var i = 0; i < 5; i++) {
setTimeout(undefined, i * 1e3);
}

而且世界输出。

1
2
3
4
5
// 0
// 1
// 2
// 3
// 4

第五版:Promise/setTimeout和Microtask/Macrotask

1
2
3
4
5
6
7
8
9
10
11
12
13
setTimeout(function() {
console.log(1);
}, 0);
new Promise(function executor(resolve) {
console.log(2);
for( var i=0 ; i<1e4 ; i++ ) {
i == 9999 && resolve();
}
console.log(3);
}).then(function() {
console.log(4);
});
console.log(5);

这里考到的知识点比较多,有ES6的Promise,还有JavaScript中的微任务(Microtask)宏任务(Macrotask)

我们知道JavaScript是单线程的,主线程一次只执行一个任务,当响应了一个事件的时候,该事件会把相应的回调函数作为任务交给主线程。但是实际上这些任务也有区别的。

像setTimeout这种函数创建的就是一个宏任务,而Promise创建的就是一个微任务,微任务的处理属于宏任务内部的事件轮询,而宏任务的处理属于主线程的事件轮询。

下面我们来详细分析上面的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 创建了一个宏任务,等待时间为1毫秒(0毫秒无效),立刻排进主线程中并继续执行当前任务
setTimeout(function() {
console.log(1);
}, 0);
// 创建一个Promise,Promise参数函数中的代码立刻执行。
new Promise(function executor(resolve) {
// 第一个输出的应该是2
console.log(2);
// 微任务加入到当前宏任务的结尾,继续执行当前任务
for( var i=0 ; i<1e4 ; i++ ) {
i == 9999 && resolve();
}
// 第二个输出的是3
console.log(3);
}).then(function() { // 定义了属于该Promise的一个微任务,继续执行当前任务
console.log(4);
});
console.log(5); // 第三个输出的是5
// 继续执行属于当前宏任务的微任务,即Promise中的then的参数函数,第四个输出的是4
// 执行完毕后执行下一个宏任务,第五个输出的是1

因此结果是。

1
2
3
4
5
// 2
// 3
// 5
// 4
// 1

关于宏任务和微任务的更多知识点,请阅读参考2参考3

参考

  1. Excuse me?这个前端面试在搞事! - Liril - 知乎
  2. HTML系列:macrotask和microtask - 杨健 - 知乎
  3. Tasks, microtasks, queues and schedules - Jake Archibald