闭包深度理解,底层实现原理
常见的闭包面试题
我们在面试到闭包的时候,总会遇到面试官问过以下几个问题。我们来简单回答下这几个问题的答案。
或者
当某个函数的作用域链还引用着其他函数的活动对象时,就会形成闭包
什么是闭包?
闭包就是一个函数(A)内部返回了一个函数(B),函数(B)引用了函数(A)的变量。
闭包有哪些实际运用场景?
- 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);
}
- 维持变量在内存中,可以做计算缓存功能。
闭包是如何产生的?
当前作用域产生了对父作用域的引用
理解闭包的前提
要理解闭包的底层原理,需要先了解几个知识点,分别是 JS执行上下文
,词法作用域
。
JS 执行上下文
JS 代码在执行的时候需要经过 V8 引擎进行预编译后才能被真正执行。这里就涉及到变量提升,函数提升。JS 运行前的预编译就是在执行上下文中进行的。
执行上下文分为三种,分别是 全局执行上下文,函数执行上下文,eval 执行上下文。
执行上下文包含三个部分
变量对象(VO),内存中建立的对象,存放当前执行环境中的变量
作用域链(Scope Chain)
this
指针
执行上下文的生命周期
- 创建阶段
创建变量对象,需要经历三个过程,分别是
创建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 指向
- 执行阶段
完成变量赋值,函数引用,执行代码。此时变量对象变为活动对象。所以上面例子的执行上下文是这样的。
// 创建过程
EC= {
VO: {}; // 创建变量对象
scopeChain: {}; // 作用域链
}
VO = {
argument: {...}; // 当前为全局上下文,所以这个属性值是空的
a: <a reference> // 函数 a 的引用地址
b: undefiend // 见上文创建变量对象的第三步
}
- 执行后出栈,等待回收
词法作用域(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
执行上下文中创建一个变量add
。add
只存在于createWarp
执行上下文中, 其函数定义存储在名为add
的自有变量中。 - 第 7 行,我们返回变量
add
的内容。js 引擎查找一个名为add
的变量并找到它. 第 4 行和第 5 行括号之间的内容构成该函数定义。 createWarp
调用完毕,createWarp
执行上下文将被销毁。add 变量也跟着被销毁。 但add
函数定义仍然存在,因为它返回并赋值给了sum
变量。 (ps:这才是闭包产生的变量存于内存当中的真相
)- 接下来就是简单的执行过程,不再赘述。。
- ……
- 代码执行完毕,全局执行上下文被销毁。sum 和 result 也跟着被销毁。
至此我们可以了解到产生闭包的整个过程以及原因,如果有理解错误可以在右侧进行反馈,一起共同进步。