跳到主内容

闭包深度理解,底层实现原理

常见的闭包面试题

我们在面试到闭包的时候,总会遇到面试官问过以下几个问题。我们来简单回答下这几个问题的答案。

或者

当某个函数的作用域链还引用着其他函数的活动对象时,就会形成闭包

什么是闭包?

闭包就是一个函数(A)内部返回了一个函数(B),函数(B)引用了函数(A)的变量。

闭包有哪些实际运用场景?

  1. for 循环解决闭包问题
for (var i = 0; i < 10; i++) {
setTimeout(() => console.log(i), 0);
}
// 控制台输出10遍10.
for (var i = 0; i < 10; i++) {
(function (a) {
setTimeout(() => console.log(a), 0);
})(i);
}
  1. 维持变量在内存中,可以做计算缓存功能。

闭包是如何产生的?

当前作用域产生了对父作用域的引用

理解闭包的前提

要理解闭包的底层原理,需要先了解几个知识点,分别是 JS执行上下文词法作用域

JS 执行上下文

JS 代码在执行的时候需要经过 V8 引擎进行预编译后才能被真正执行。这里就涉及到变量提升,函数提升。JS 运行前的预编译就是在执行上下文中进行的。

执行上下文分为三种,分别是 全局执行上下文,函数执行上下文,eval 执行上下文。

执行上下文包含三个部分

  1. 变量对象(VO),内存中建立的对象,存放当前执行环境中的变量

  2. 作用域链(Scope Chain)

  3. this 指针

执行上下文的生命周期

  1. 创建阶段

创建变量对象,需要经历三个过程,分别是

  • 创建arguments对象

对于函数执行环境,查询是否有实参,如果有,将函数名是实参值组成的键值对放到 arguments 对象。如果没有则将参数名和 undefined 的键值对放到 arguments 对象中。举个 🌰

function bar(a, b, c) {
console.log(arguments); // [1, 2]
console.log(arguments[2]); // undefined
}
bar(1, 2);
  • 遇到同名函数,后面覆盖前面,举个 🌰,在执行 a 的时候,函数声明已经完成了,所以可以执行。
console.log(a); // function a() {console.log('Is a ?') }
function a() {
console.log("Is 1024nav");
}
function a() {
console.log("Is 1024nav ?");
}
  • 变量赋值为 undefined

把当前执行环境中的变量声明并赋值为 undefined。遇到同名的函数声明时,为了避免函数被赋值为 undefined,直接跳过。。

console.log(a); // function a() {console.log('Is 1024nav ?') }
console.log(b); // undefined
function a() {
console.log("Is 1024nav ");
}
function a() {
console.log("Is 1024nav ?");
}
var b = "Is b";
var a = 1024;
console.log(a); // 1024

建立作用域链,确定 this 指向

  1. 执行阶段

完成变量赋值,函数引用,执行代码。此时变量对象变为活动对象。所以上面例子的执行上下文是这样的。

// 创建过程
EC= {
VO{}; // 创建变量对象
scopeChain: {}; // 作用域链
}
VO = {
argument: {...}; // 当前为全局上下文,所以这个属性值是空的
a: <a reference> // 函数 a 的引用地址
b: undefiend // 见上文创建变量对象的第三步
}
  1. 执行后出栈,等待回收

词法作用域(Lexical scope)

当 JS 在查找变量的时候,会在当前上下文中查找,没有找到就会去上一级的上下文中查找,一直到最后的全局上下文中查找,如果还找不到,值就是 undefined

接下来我们来举一个闭包的例子。

 1: let top = 0; //
2: function createWarp() {
3: function add(a, b) {
4: let ret = a + b
5: return ret
6: }
7: return add
8: }
9: let sum = createWarp()
10: let result = sum(top, 8)
11: console.log('result:',result)

分析过程如下:

  • 在全局上下文中声明变量top 并赋值为 0.
  • 2 - 8 行。在全局执行上下文中声明了一个名为 createWarp 的变量,并为其分配了一个函数定义。其中第 3-7 行描述了其函数定义,并将函数定义存储到那个变量(createWarp)中。
  • 第 9 行。我们在全局执行上下文中声明了一个名为 sum 的新变量,暂时,值为 undefined
  • 第 9 行。遇到(),表明需要执行或调用一个函数。那么查找全局执行上下文的内存并查找名为 createWarp 的变量。 明显,已经在步骤 2 中创建完毕。接着,调用它。
  • 调用函数时,回到第 2 行。创建一个新的createWarp执行上下文。我们可以在 createWarp 的执行上下文中创建自有变量。js 引擎createWarp 的上下文添加到调用堆栈(call stack)。因为这个函数没有参数,直接跳到它的主体部分.
  • 3 - 6 行。我们有一个新的函数声明,createWarp执行上下文中创建一个变量 addadd 只存在于 createWarp 执行上下文中, 其函数定义存储在名为 add 的自有变量中。
  • 第 7 行,我们返回变量 add 的内容。js 引擎查找一个名为 add 的变量并找到它. 第 4 行和第 5 行括号之间的内容构成该函数定义。
  • createWarp 调用完毕,createWarp 执行上下文将被销毁。add 变量也跟着被销毁。add 函数定义仍然存在,因为它返回并赋值给了 sum 变量。 (ps: 这才是闭包产生的变量存于内存当中的真相
  • 接下来就是简单的执行过程,不再赘述。。
  • ……
  • 代码执行完毕,全局执行上下文被销毁。sum 和 result 也跟着被销毁。

至此我们可以了解到产生闭包的整个过程以及原因,如果有理解错误可以在右侧进行反馈,一起共同进步。