一道简单的预编译、作用域、闭包面试题

73 阅读3分钟

面试中会问到的一个比较基础的问题,鉴于目前大家可能直接从 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 执行流程之词法环境、作用域

一个简单的面试题,设计到的基础知识点有:

  • 预编译
  • 词法环境、闭包、作用域、作用域链

还是要认真对待的。

而且,对很多可能用的很熟练的大佬,也不知道具体原因是为啥。

想在这条路上走的更远更久,每一个基础知识点都是有用的。

后面会出有关 预编译 闭包 作用域的文章,唉,其实明白的人都知道,所谓的什么,执行上下文、词法环境、作用域、闭包,说的都是一个东西。这部分写起来有点麻烦,一定会让基础不牢的同学真正理解,可以关注下我的专栏,等待后续更新。

你不知道的JavaScript - ThisGravity的专栏 - 掘金 (juejin.cn)