闭包相关的面试题

阅读本文之前,默认大家理解闭包的相关知识,尤其是自由变量的查找。如果不了解,建议去读我的另一篇文章搞懂JavaScript的作用域、作用域链、闭包,理解之后再来阅读本文,效果更佳。

1. 循环与闭包

1.1 从一个例子引入

将循环和闭包结合起来,是一种非常经典的命题方式。

首先来看一个非常常见的例子。

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

console.log(i); // 输出什么?

最后一行代码输出5,大家肯定都没有疑问。这道题的关键在于for循环中的代码的输出。

for循环中的代码执行了5次,每一次都会将里面的函数延迟1000ms执行。这里面的函数长这个样子:

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

这个函数中根本没有i这个变量。这种在函数中没有被定义,却被使用了的变量,就是自由变量。(自由变量在我的这篇博客中做了详细介绍,如果不了解,可以去看看:搞懂JavaScript的作用域、作用域链、闭包

自由变量的查找,要到上级作用域中去一层一层寻找。

画出整段代码对应的作用域,应该长这样:

在这里插入图片描述

当for循环中的输出语句被执行时,至少也是1000ms之后的事情了,在这之前,5次循环早已经被执行完毕,全局作用域中的i的值已经变成了5。当输出语句执行时,会沿着作用域找到全局作用域中的i。所以会输出5。

一共进行了5次循环,每一次循环输出的都是全局作用域中的i,自然会输出五个5。

所以,这段代码的输出结果应该是:

5 5 5 5 5 5

1.2 上面的例子,如何改造?

如果我们希望i能按顺序输出0 1 2 3 4,那么要怎么改造这段代码?

1.2.1 利用setTimeout的第三个参数

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

我们只需要把每一轮循环中i的值,放入setTimeout第三个参数中去:

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

console.log(i);
1.2.2 外层包裹一层函数

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

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

for (var i = 0; i < 5; i++) {
    // 这里的 i 被赋值给了 out 作用域内的变量 i
    out(i);  
}
1.2.3 利用IIFE——立即执行函数

同样,要在setTimeout的外面再套一层函数,只不过这个函数是一个立即执行函数(不懂IIFE的看这里)。同样要给这个立即执行函数传递参数,传递的参数就是i

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

1.3 真题演练

来看两道闭包+循环的题目。分析输出是什么。

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

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

    return num[9];
}

test()() // 输出什么?

答案:10

解析:这道题中首先执行test(),这个函数中进行了十次循环,每次循环都往数组num中新增一个function(这个function这里还没有执行,只是把函数的定义作为数组元素扔进去),最后返回下标为9的这个function。所以,这里通过使用函数作为返回值的形式,形成了一个闭包。接着,test()()执行的是nums[9]这个function,也就是for循环中的这个function,把他拎出来:

function () {
	console.log(i)
}

这个函数要打印i的值,但是这个函数中本身并没有定义i这个变量,所以变量i是一个自由变量。自由变量的查找,是要到函数定义的地方,沿着作用域链向上级查找(这一点在搞懂JavaScript的作用域、作用域链、闭包这篇文章中讲过)。

于是,就找到了test()这个函数的作用域。这个作用域中定义了变量i。当我执行打印i的操作时,for循环早就已经执行完毕了,i的值已经变成了10,所以此时找到的i,它的值就是10。所以,这道题的输出结果就是10。

再来看一道题。

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

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

console.log(test()) // 输出什么?

答案:10。

解析:test的值是一个IIFE,在定义时就会立即执行。这个IIFE返回一个箭头函数。接着执行for循环,循环10次,每次执行都会执行test(),也就是刚才返回的那个箭头函数。返回一个函数,形成了一个闭包。

在这个箭头函数中,要返回一个num++,但是在这个函数中没有定义num,这是一个自由变量,所以要在函数定义的地方沿着作用域链向上查找。于是,在IIFE的作用域中,找到了num = 0,然后执行num++,此时作用域中的num值变为1。

因为要执行10次循环,每次循环都会使IIFE中的num值自增1,所以在10次循环之后,num的值变为10。

最后输出的时候,又一次执行了test(),但是由于返回的是num++,所以先打印出num的值,然后才会自增。

所以这道题的答案是10。

2. 复杂作用域

除了循环+闭包这个知识点,还有一个考察闭包的方向,就是复杂作用域。

这类题目的特点就是,会搞出很多作用域混在一起,把人弄懵。

其实对于复杂的作用域,只要仔细分析,这类题目并不难。

看例题。

2.1 例题1

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

先来分析一下作用域。

全局作用域中定义了 var a = 1,定义了一个函数test()

函数test()的作用域中,有一个a = 2,还有一个var a = 3。这个函数返回了一个函数。注意,函数里返回一个函数,第一时间就要想到闭包。

这里隐藏了一个知识点:变量提升。函数中,先出现了a = 2,后面又来一个var a = 3。函数作用域的内部,也是存在变量提升的,所以,关于变量a的声明,会被提升到函数顶部。所以,test()函数其实等价于这样:

function test(){
    // 变量提升
    var a = 2;
    return function(){
        console.log(a);
    }
    a = 3;
}

现在来分析一下整段代码的执行过程。

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

先执行test(),返回了一个函数,然后再执行这个返回的函数。在这个返回的函数中,要打印a的值,这个a没有在这个返回的函数中定义,属于自由变量。又是寻找自由变量,这个过程已经很熟悉了。去函数定义的地方,沿着作用域链向上级进行查找。于是,找到了test()函数作用域中的var a = 2(刚才讲到的变量提升)。于是顺理成章地输出2。

2.2 练习

做一道练习题:

function foo(a,b){
  console.log(b);
  return {
    foo:function(c){
      return foo(c,a);
    }
  }
}
 
var func1=foo(0);
func1.foo(1);
func1.foo(2);
func1.foo(3);
var func2=foo(0).foo(1).foo(2).foo(3);
var func3=foo(0).foo(1);
func3.foo(2);
func3.foo(3);

这段代码输出什么?

答案在下面。

答案:

依次输出:

undefined 0 0 0
undefined 0 1 2
undefined 0 1 1

这道题的作用域比较复杂,但并不难,仔细分析就可以得出答案。

以上就是关于闭包的一些面试题,如果有问题,欢迎给我留言~

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值