一道面试题有多少知识点(ES6,箭头函数,闭包,EventLoop)

1
2
3
4
5
6
7
for (var i = 0; i < 10; i++) {
setTimeout(() => {
console.log(i);
}, 0);
}
// 输出
// 10 个 10

前言

这是一道常见的面试题,一般大家都问代码输出什么,考的是var,ES6 的letfor闭包,之后还可能会问你箭头函数,然后再问你setTimeout原理甚至到 Micro-Task 和 Macro-Task 我们一个一个攻破。

var let

1
2
3
4
5
6
7
8
9
10
11
12
{
var foo = "foo";
}
console.log(foo); // foo
var foo = "foo1";
console.log(foo); // foo1

{
let bar = "bar";
// let bar = 'bar1'; // SyntaxError: Identifier 'bar' has already been declared
}
console.log(bar); // ReferenceError: bar is not defined

varlet最最大区别就在这里,var并没有作用域这一说,还能重复声明,let只允许存在于自己的作用域,也不允许重复声明。

for

for在实现的时候要求,每次迭代会新建运行环境记录拷贝最后迭代内容,所以每一次都是声明在一个独立的作用域,所以改用let可以输出 1 2 3 4 5…
规范

13.7.4.7 Runtime Semantics: LabelledEvaluation
13.7.4.8 Runtime Semantics: ForBodyEvaluation( test, increment, stmt, perIterationBindings, labelSet )
13.7.4.9 Runtime Semantics: CreatePerIterationEnvironment( perIterationBindings )

闭包

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

闭包解决这个问题的具体原因就是通过闭包将变量转为参数,如果是基本类型的参数会进行值传递,但是如果是Object类型的会通过引用传递。
闭包更常用的用法就是工厂模式的方法进行生产方法,比如 redis 存储的时候经常有一些前缀,但是不多,不同用途不同前缀,每一个独立方法或者通过参数传递变得麻烦,就可以通过闭包形式进行

1
2
3
4
5
6
7
8
9
10
11
function generateRedisStore(prefix) {
return function(key) {
return redis.get(`${prefix}:${key}`);
};
}

const tokenRedis = generateRedisStore("token");

tokenRedis.get("userId").then(() => {
// doSomething
});

箭头函数

1
2
3
4
5
6
7
8
9
this.value = "value";

setTimeout(function() {
console.log(this.value); // undefined
}, 0);

setTimeout(() => {
console.log(this.value); // value
}, 0);

箭头函数在别的语言可能就是个语法糖,在 JavaScript 上就不同了…他可以改变this的指向,本来我们的function里面也有自己的this,但是如果使用箭头函数,就放弃了functionthis

一些坑点

  • 由于箭头函数没有自己的this,所以如果调用call或者apply只能传参数而不能绑定this

  • 箭头函数不能作为constructor,如果和new一起使用会报错。

  • 箭头函数没有prototype属性

setTimeout

setTimeout就不得不先讲 EventLoop,众所周知 JavaScript 是单线程异步,有异步,实际上一定有多条线程,只是,我们接触不到而已,而浏览器和 Node.js 环境又有一点不同,这里就不细讲了,简单讲讲关于 Micro-Task 和 Macro-Task

为了清晰,我们先区分三个队列,Main-Task,Micro-Task 和 Macro-Task

  • Main-Task 就是一开始我们代码运行的 Task,这个 Task 如果没有运行完,其他 Task 无法参与。
  • Micro-Task 是Promise为首的 Task,浏览器还多一个 MutationObserver。
  • Macro-Task 比如 setTimeoutsetIntervalsetImmediateI/OUI rendering<script>都是。

简单来讲 EventLoop 就是不断的执行一个流程,从 Main-Task 开始,执行完了到 Micro-Task 然后 Macro-Task,所以其实 setTimeout 在到 Macro-Task 如果想要执行,还是得等到前面两个 Task 完全清空,才轮到你。

所以无论如何,都会在 Main-Task 之后执行。

EventLoop 更详细的解答可以参考Eventloop 不可怕,可怕的是遇上 Promise,其实一旦理解了Promise.then是增加到 Micro-Task 就基本能理解其他了。

注意async/await在不同版本上有不同实现,V8/Node.js 12 之前需要经过三轮的 Micro-Task 才完成await