是什么
闭包是指那些能够访问自由变量的函数。
那什么是自由变量呢?
自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量。
由此,我们可以看出闭包共有两部分组成:
闭包 = 函数 + 函数能够访问的自由变量
题目
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}
data[0]();
data[1]();
data[2]();
// 输出 3 3 3
当执行到 data[0] 函数之前,此时全局上下文的 VO 为:
globalContext = {
VO: {
data: [...],
i: 3
}
}
当执行 data[0] 函数的时候,data[0] 函数的作用域链为:
data[0]Context = {
Scope: [AO, globalContext.VO]
}
data[0]Context 的 AO 并没有 i 值,所以会从 globalContext.VO 中查找,i 为 3,所以打印的结果就是 3。
data[1] 和 data[2] 是一样的道理。
改成闭包
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = (function (i) {
return function(){
console.log(i);
}
})(i);
}
data[0]();
data[1]();
data[2]();
//0 1 2
当执行到 data[0] 函数之前,此时全局上下文的 VO 为:
globalContext = {
VO: {
data: [...],
i: 3
}
}
跟没改之前一模一样。
当执行 data[0] 函数的时候,data[0] 函数的作用域链发生了改变:
data[0]Context = {
Scope: [AO, 匿名函数Context.AO globalContext.VO]
}
匿名函数执行上下文的AO为:
匿名函数Context = {
AO: {
arguments: {
0: 0,
length: 1
},
i: 0
}
}
data[0]Context 的 AO 并没有 i 值,所以会沿着作用域链从匿名函数 Context.AO 中查找,这时候就会找 i 为 0,
找到了就不会往 globalContext.VO 中查找了,即使 globalContext.VO 也有 i 的值(值为3),所以打印的结果就是0。
data[1] 和 data[2] 是一样的道理。
为什么
-
数据封装与隐私:闭包允许创建私有变量,这些变量只能被闭包内部的函数访问,从而提供了数据的封装性。这有助于隐藏内部实现细节,保护数据不被外部随意修改,维护了数据的隐私性。
-
状态保持:闭包可以记住并访问其创建时的作用域中的变量,即使这些变量已经不在当前作用域中了。这种“记忆”功能使得闭包能够保持状态,这对于需要持久化数据的场景非常有用,比如实现具有私有状态的模块或对象。
-
函数工厂:闭包可以用来创建函数工厂,即返回一个函数的函数。这使得可以动态地创建具有特定行为的函数,这些函数可以访问并操作闭包作用域中的变量,从而提供了高度的灵活性和可定制性。
-
模块化:通过闭包,可以模拟出模块化的代码结构。每个模块可以包含私有变量和函数,以及对外暴露的公共接口。这种模块化的方式有助于减少全局变量的使用,提高代码的组织性和可维护性。
-
简化回调函数的使用:在JavaScript中,回调函数非常常见,特别是在处理异步操作时。闭包使得回调函数可以访问到定义它们的函数作用域中的变量,这简化了回调函数的编写,使得它们能够更容易地访问和操作所需的数据。
-
代码复用:闭包提供了一种将数据和函数绑定在一起的方式,这使得代码更加模块化和可复用。通过闭包,可以将一组相关的数据和操作封装在一起,形成一个独立的单元,这个单元可以在不同的地方被重用。
怎么办
例子
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
var foo = checkscope();
console.log(foo());
// 输出 local scope
这里直接给出简要的执行过程:
- 进入全局代码,创建全局执行上下文,全局执行上下文压入执行上下文栈
- 全局执行上下文初始化
- 执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 执行上下文被压入执行上下文栈
- checkscope 执行上下文初始化,创建变量对象、作用域链、this等
- checkscope 函数执行完毕,checkscope 执行上下文从执行上下文栈中弹出
- 执行 f 函数,创建 f 函数执行上下文,f 执行上下文被压入执行上下文栈
- f 执行上下文初始化,创建变量对象、作用域链、this等
- f 函数执行完毕,f 函数上下文从执行上下文栈中弹出
了解到这个过程,我们应该思考一个问题,那就是:
当 f 函数执行的时候,checkscope 函数上下文已经被销毁了啊(即从执行上下文栈中被弹出),怎么还会读取到 checkscope 作用域下的 scope 值呢?
然而 JavaScript 却是可以的!
当我们了解了具体的执行过程后,我们知道 f 执行上下文维护了一个作用域链:
fContext = {
Scope: [AO, checkscopeContext.AO, globalContext.VO],
}
对的,就是因为这个作用域链,f 函数依然可以读取到 checkscopeContext.AO 的值,说明当 f 函数引用了 checkscopeContext.AO 中的值的时候,即使 checkscopeContext 被销毁了,但是 JavaScript 依然会让 checkscopeContext.AO 活在内存中,f 函数依然可以通过 f 函数的作用域链找到它,正是因为 JavaScript 做到了这一点,从而实现了闭包这个概念。
所以,让我们再看一遍实践角度上闭包的定义:
- 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
- 在代码中引用了自由变量
总结
在某个内部函数的执行上下文创建时,会将父级函数的活动对象加到内部函数的 [[scope]]
中,形成作用域链,所以即使父级函数的执行上下文销毁(即执行上下文栈弹出父级函数的执行上下文),但是因为其活动对象还是实际存储在内存中可被内部函数访问到的,从而实现了闭包。
闭包:自由变量的查找,是在函数定义的地方,向上级作用域查找。不是在执行的地方。