十个用图表解释JavaScript 闭包的面试题

开发 前端
闭包是函数式编程中的核心概念之一,是每个 JavaScript 开发人员必备的知识。在这里,我准备了 10 个关于闭包的面试挑战题,这些基本都是面试中经常被问到的。

你准备好了吗?我们现在要开始了。

每个题目都有一个代码片段,你需要说出这段代码的输出是什么。

1、范围

在说闭包之前,我们必须了解作用域的概念,它是理解闭包的基石。

此代码段的输出是什么?

var a = 10
function foo(){
   console.log(a)
}
foo()
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

这很简单,相信所有人都知道输出结果是10。

  • 默认情况下,有一个全局范围。
  • 本地作用域由函数或代码块创建。

当执行 console.log(a) 时,JavaScript 引擎将首先在函数 foo 创建的本地范围内查找 a。当 JavaScript 引擎找不到 a 时,它会尝试在其外部作用域(即全局作用域)中查找 a。然后事实证明a的值为10。

2、 局部作用域

var a = 10
function foo(){
   var a = 20
   console.log(a)
}
a = 30
foo()
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

在这段代码中,变量 a 也存在于 foo 的范围内。所以当执行 console.log(a) 时,JavaScript 引擎可以直接从本地作用域获取 a 的值。

所以输出是 20 。

记住:当 JavaScript 引擎需要查询一个变量的值时,它会首先在本地范围内查找,如果没有找到该变量,它会继续在上层范围内查找。

3、词法作用域

var a = 10
function foo(){
   console.log(a)
}
function bar() {
   var a = 20
   foo()
}
bar()
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

这个问题容易出错,也是面试中经常出现的问题,你可以考虑一下。

简单地说,JavaScript 实现了一种名为词法作用域(或静态作用域)的作用域机制。它被称为词法(或静态),因为引擎仅通过查看 JavaScript 源代码来确定范围的嵌套,无论它在哪里调用。

所以输出是 10 :

4、修改词法作用域

如果我们将代码片段更改为:

var a = 10
function bar() {
 var a = 20
 function foo(){
   console.log(a)
 }
 foo()
}
bar()
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

输出是什么?

foo 范围成为 bar 范围的子范围:

当 JavaScript 引擎在 Foo 作用域中没有找到 a 时,它会首先从 Foo 作用域的父作用域,也就是 Bar 作用域中寻找 a,它确实找到了 a。

所以输出是 20:

好了,以上就是关于范围的一些基本挑战,相信你能顺利通过。现在我们开始进入闭包的部分。

5、 闭包

function outerFunc() {
 let a = 10;
 function innerFunc() {
   console.log(a);
 }
 return innerFunc;
}
let innerFunc = outerFunc();
innerFunc()
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

输出是什么?这段代码会抛出异常吗?

在词法范围内,innerFunc 仍然可以访问 a,即使在其词法范围之外执行。

换句话说,innerFunc 从其词法范围中记住(或关闭)变量 a。

换句话说,innerFunc 是一个闭包,因为它在变量 a 的词法范围内关闭。

因此,这段代码不会抛出异常,而是输出 10。

6、 IIFE

(function(a) {
 return (function(b) {
   console.log(a);
 })(1);
})(0);
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

此代码片段使用 JavaScript 立即调用函数表达式 (IIFE)。

我们可以简单地将这段代码翻译成这样:

function foo(a){
 function bar(b){
   console.log(a)
 }
 return bar(1)
}
foo(0)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

所以输出是 0 。

闭包的一个经典应用是隐藏变量。

比如现在要写一个计数器,基本的写法是这样的:

let i = 0
function increase(){
 i++
 console.log(`courrent counter is ${i}`)
 return i
}
increase()
increase()
increase()
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

可以这样写,但是在全局范围内会多出一个变量i,这样就不好了。

这时候,我们可以使用闭包来隐藏这个变量。

let increase = (function(){
 let i = 0
 return function(){
   i++
   console.log(`courrent counter is ${i}`)
   return i
 }
})()
increase()
increase()
increase()
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

这样,变量 i 就隐藏在局部范围内,不会污染全局环境。

7、多重声明和使用

let count = 0;
(function() {
 if (count === 0) {
   let count = 1;
   console.log(count);
 }
 console.log(count);
})();
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

在这个代码片段中,有两个 count 的声明和三个 count 的用法。这是一个难题,你应该仔细考虑。

首先,我们要知道if代码块也创建了一个局部作用域,上面的作用域大致是这样的。

  • Function Scope 没有声明自己的计数,所以我们在这个作用域中使用的计数是全局作用域的计数。
  • If Scope 声明了自己的计数,所以我们在这个作用域中使用的计数就是当前作用域的计数。

或在此图中:

所以输出是 1 , 0 :

8、调用多个闭包

function cr
eateCounter(){
let i = 0
return function(){
   i++
return i
 }
}
let increase1 = createCounter()
let increase2 = createCounter()
console.log(increase1())
console.log(increase1())
console.log(increase2())
console.log(increase2())
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.

这里需要注意的是,increase1和increase2是通过不同的函数调用createCounter创建的,它们不共享内存,它们的i是独立的,不同的。

所以输出是 1 , 2 , 1 , 2 。

9、返回函数

function createCounter() {
 let count = 0;
  function increase() {  
   count++;
 }
 let message = `Count is ${count}`;
 function log() {
   console.log(message);
 }
 return [increase, log];
}
const [increase, log] = createCounter();
increase();  
increase();  
increase();  
log();
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.

这段代码很容易理解,但是有个陷阱:message其实是一个静态字符串,它的值固定为Count为0,当我们调用increase或者log时不会改变。

所以每次调用 log 函数,输出结果总是 Count is 0 。

如果您希望 log 函数及时检查 count 的值,请将 message 移入 log :

function createCounter() {
 let count = 0;
  function increase() {  
   count++;
 }
-  let message = `Count is ${count}`;
 function log() {
+  let message = `Count is ${count}`;
   console.log(message);
 }
 return [increase, log];
}
const [increase, log] = createCounter();
increase();  
increase();  
increase();  
log();
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.

10、异步闭包

for (var i = 0; i < 5; i++) {
 setTimeout(function () {
   console.log(i);
 }, 0)
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

输出是什么?

上面的代码等价于:

var i = 0;
setTimeout(function(){
 console.log(i);
},0)
i = 1;
setTimeout(function(){
 console.log(i);
},0)
i = 2;
setTimeout(function(){
 console.log(i);
},0)
i = 3;
setTimeout(function(){
 console.log(i);
},0)
i = 4;
setTimeout(function(){
 console.log(i);
},0)
i = 5
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.

而且我们知道JavaScript会先执行同步代码,然后再执行异步代码。所以每次执行console.log(i)时,i的值已经变成了5。

所以输出是 5 , 5 , 5 , 5 , 5 。

如果我们想要代码输出 0 , 1 , 2 , 3 , 4 ,需要怎么操作?

使用闭包的解决方案是:

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

上面的代码等价于:

var i = 0;
(function(cacheI){setTimeout(function(){
 console.log(cacheI);
},0)})(i)
i = 1;
(function(cacheI){setTimeout(function(){
 console.log(cacheI);
},0)})(i)
i = 2;
(function(cacheI){setTimeout(function(){
 console.log(cacheI);
},0)})(i)
i = 3;
(function(cacheI){setTimeout(function(){
 console.log(cacheI);
},0)})(i)
i = 4;
(function(cacheI){setTimeout(function(){
 console.log(cacheI);
},0)})(i)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.

我们通过 JavaScript 立即调用的函数表达式创建函数范围。i 的值是通过闭包保存的。

恭喜你,到这里,你已经学会了这些面试挑战题。

希望在开发面试中,闭包相关的问题不会再困扰你了。

最后,感谢你的阅读,如果你觉得有用的话,请点赞我,关注我,并将其分享给你的身边做开发的朋友,也许能够帮助到他。

责任编辑:庞桂玉 来源: web前端开发
相关推荐

2025-03-18 12:00:00

闭包JavaScript前端

2022-02-11 10:16:50

MySQLDBA数据库

2016-01-11 11:50:39

JavaScript闭包面试题

2021-03-31 10:36:33

Python面试题开发

2015-08-27 09:27:34

JavaScript面试题

2017-03-24 09:37:45

前端开发者JavaScript面试题

2021-03-01 09:39:34

闭包JavaScript开发

2022-11-25 14:55:43

JavaScriptweb应用程序

2017-11-06 13:02:37

前端setTimeout循环闭包

2024-06-04 14:52:28

2013-01-05 14:51:34

JavaScriptjQuery面试

2015-07-31 09:34:44

Java面试题

2015-11-25 10:48:44

JS闭包面试题

2023-10-16 07:55:15

JavaScript对象技巧

2024-11-28 08:33:16

JavaScrip事件循环this

2023-02-14 08:10:14

Python人工智能XAI

2025-01-09 12:00:00

JavaScript前端数组

2022-11-28 15:04:42

数据可视化工具

2023-05-18 15:34:52

JavaScript开发前端

2023-09-04 15:48:05

JavaScript语言
点赞
收藏

51CTO技术栈公众号