面试中会问到的一个比较基础的问题,鉴于目前大家可能直接从 let、const 学起,可能对这个不是太了解,就分享一下。
问题
以下代码输出什么?
for (var i = 0; i < 10; i++) {
setTimeout(() => {
console.log(i);
}, 0);
}
输出:
10 x 10 // 10 个 10
为什么?
因为,👆 的代码预编译(执行前)后为:
var i = undefined; // var 变量提升
for (i = 0; i < 10; i++) {
setTimeout(() => {
console.log(i);
}, 0);
}
编译完后,代码执行,每次循环都会向宏任务队列添加一个超时任务,循环会执行 10 次,往宏任务队列中添加了 10 个回调函数,每一个回调函数最终都会输出 i。
当 i = 10 时,循环结束,此时,同步代码执行完毕,开始到任务队列中取任务执行,因此,每一个 console.log(i) 打印的都是 10。
注意:
- var 变量提升,涉及 JS 代码编译原理
- (i = 0; i < 10; i++) 是一个单独的作用域,这里为了简单,先不扩展
怎么解决
在不使用 let const 的情况下,怎么解决?
最典型,最常用的是使用闭包
直接上代码:
for (var i = 0; i < 10; i++) {
(function (j) {
setTimeout(() => {
console.log(j); // 这里引用了外部变量,产生了闭包
}, 0);
})(i);
}
为什么上面的方法可以解决?
我们梳理一下这段 JS 代码的执行流程
同步执行流程:
- 每次循环都会创建一个新的函数,并将当前
i作为参数,传递给j,当前的 i 值,也就成为了函数的一个局部变量 - 函数中的 setTimeout 的回调中使用到了外部的
j,这里产生了闭包,外部函数的作用域不会被垃圾回收,而是保存在回调函数体内(作用域链:[[Scopes]])。 - 这样循环 10 次,往任务队列添加 10 个回调
异步执行流程:
- 主线程空闲后,到任务队列依次取出这 10 个回调执行
- 在执行
console.log(j)时,因为当前函数作用域没有j变量,因此会沿着作用域链查找上层作用域,上层作用域也就是我们每次 for 循环创建的那个函数,里面有一个局部变量j,值为当前的i的值 - 输出 i 为 0、1、2、...、9
注意:
- 闭包的本质
- JS 执行流程之词法环境、作用域
一个简单的面试题,设计到的基础知识点有:
- 预编译
- 词法环境、闭包、作用域、作用域链
还是要认真对待的。
而且,对很多可能用的很熟练的大佬,也不知道具体原因是为啥。
想在这条路上走的更远更久,每一个基础知识点都是有用的。
后面会出有关 预编译 闭包 作用域的文章,唉,其实明白的人都知道,所谓的什么,执行上下文、词法环境、作用域、闭包,说的都是一个东西。这部分写起来有点麻烦,一定会让基础不牢的同学真正理解,可以关注下我的专栏,等待后续更新。