图解一道经典闭包面试题,剖析闭包面试考什么

1,015 阅读2分钟

这是我参与8月更文挑战的第11天,活动详情查看8月更文挑战

前言

上次,我们通过这篇文章JavaScript 的闭包,可以这样理解,让大家对闭包有了一个不一样的理解。今天我们从一道经典面试题出发,剖析 JavaSctipt 闭包面试考什么。

知己知彼百战百胜

“循环体与闭包”系列

把闭包和循环体结合起来考察,是闭包最为经典的一种命题方式。

熟悉但有可能答错的题

我们来看一下这个代码

for (var i = 0; i < 5; i++) {
    setTimeout(function() {
        console.log(i);
    }, 1000);
}

console.log(i);

请问:上面代码输出的结果是什么?可以将自己的答案记下来,继续往下看

第一种答案:

0 1 2 3 4 5

为什么会想到这样的答案呢?一般,是刚入门的新手,不熟悉 setTimeout 的用法和作用,导致对代码实际执行的顺序理解错误。

第二种答案:

5 0 1 2 3 4

首先给出这种答案的同学,说明他是对 setTimeout 的用法和作用有相关了解,但是对于作用域、闭包等相关知识不太熟悉,认为会输出 0 1 2 3 4。

正确答案:

5 5 5 5 5 5

为什么是这个答案呢? 首先,最后一行的打印,输出 5,大家基本都是知道的,其实就是同步和异步的区别; 其次,大家也知道 setTimeout 函数会被推迟执行; 最终,我们来看看为什么 setTimeout 打印的结果都是 5。

最简单的理解 因为 for 循环是很快的,当执行完 for 循环,变量 i 已经变成了 5,for 循环里面的 setTimeout 执行了 5 次,每次都会将这个函数的执行推迟 1000 ms,然后打印变量 i,打印的都是 5。

image.png

更深入的解答 先来个文字版

当下面这个函数被调用的时候,使用到变量 i,但是当前作用域没有变量 i,那么这个时候,JS 引擎探出头去,去上层作用域(全局作用域)找变量 i。此时 for 循环早已经执行完毕,i 的值,已经是 5。

当 1000ms 后,这个函数第一次被执行的时候,打印的变量 i 的已经是 5。

function() {
    console.log(i);
}

我们来画图加深理解

作用域关系示意如下:

image.png

作用域链关系展示如下:

image.png

我们结合文字版和图片,能够分析出,每隔 1000 ms,setTimeout 回调被执行的时候,都是去全局作用域,拿变量 i,所以每一次输出的结果都是 5。

改造代码,输出目标结果

现在的输出结果:

5 5 5 5 5 5

期望的目标结果:

5 0 1 2 3 4

思路1:setTimeout 的第三个参数

说明:setTimeout 从第三个入参位置开始往后,是可以传入无数个参数的。这些参数会作为回调函数的附加参数存在。

将每一轮循环的 i 的值,存储到 setTimeout 的第三个参数,回调函数的形参 j,会接收到每一轮循环的 i,所以可以输出正确结果。

for (var i = 0; i < 5; i++) {
    setTimeout(function(j) {
        console.log(j);
    }, 1000, i);
}

console.log(i);

思路2:外部函数入参

在 setTimeout 外面再套一层函数,利用这个外部函数的入参来缓存每一个循环中的 i 值:

var output = function (i) {
    setTimeout(function() {
        console.log(i);
    }, 1000);
};

for (var i = 0; i < 5; i++) {
    // 这里的 i 被赋值给了 output 作用域内的变量 i
    output(i);  
}

我们看图,加深理解 image.png

思路3:立即执行函数入参

第二种思路,需要单独声明一个函数,多声明一个全局变量,我们可以改成立即执行函数,同样也是利用函数的入参来缓存每一个循环中的 i 的值

注:变量 j,其实是立即执行函数的局部变量,为了不让大家混淆实参 i,所以改成 j

for (var i = 0; i < 5; i++) {
    // 这里的 i 被赋值给了立即执行函数作用域内的变量 j
    (function(j) {  
        setTimeout(function() {
            console.log(j);
        }, 1000);
    })(i);
}

我们画图,加深理解

image.png

思路4:ES6 的 let

其实就是将 var 声明改成 let 声明,let 声明,有块级作用域,找变量 i 的时候,是直接找到对应块级作用域的变量 i。

for (let i = 0; i < 5; i++) {
    setTimeout(function() {
        console.log(i);
    }, 1000);
}

console.log(i);

改变一下下

题一

function test (){
    var num = []
    var i

    for (i = 0; i < 10; i++) {
        num[i] = function () {
            console.log(i)
        }
    }

    return num[9]
}

test()()

输出结果:10

这里就不文字描述了,直接看图理解吧。 image.png

题二

var test = (function() {
    var num = 0
    return () => {
        return num++
    }
}())

for (var i = 0; i < 10; i++) {
    test()
}

console.log(test())

输出结果:10

需要注意的是变量 test 对应的值,是立即执行返回的结果

() => {
    return num++
}

“复杂作用域”系列

它的套路非常简单粗暴 —— 就是在一道题里面,尽可能地想办法给你折腾出一堆作用域(有时还会杂糅一些较为零碎的 JS 语法知识点),目的就是把你整懵。

做题技巧 没有别的,就是画图,从被执行的函数看起,一层一层由内往外地把作用域关系图给画出来,图出来了,基本上答案,也就出来了。 步骤:

  1. step1:读题
  2. step2:画图
    1. 分层:我们从内向外画。
    2. 找变量:这是这题的难点!

实践出真知

我们来做一道题

var a = 1;
function test(){
    a = 2;
    return function(){
        console.log(a);
    }
    var a = 3;
}
test()();

假设大家对变量提升和作用域规则这两个知识点,是熟悉的,如果不熟悉,请看下这两篇文章

因为使用 var 声明存在变量提升,所以实际运行的代码,是这样的:

var a = 1;
function test(){
    var a = 2;
    return function(){
        console.log(a);
    }
    a = 3;
}
test()();

其次,作用域规则

我们作用域的划分,是在书写的过程中,根据你把它写在哪个位置来决定的。像这样划分出来的作用域,遵循的就是词法作用域模型。

所以 test 作用域的变量的值,实际上是 2。

下面我们来画图吧

作用域关系示意如下:

image.png

作用域链关系展示如下:

image.png

到这里,输出结果已经清晰明了,就是 2。

参考

文中如有错误,欢迎在评论区指正,如果这篇文章帮助到了你或者喜欢,欢迎点赞和关注。