开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第5天,点击查看活动详情
在作用域和作用域链的文章中,我们知道函数的[[Scopes]]属性用来描述当前函数的作用域,其中存在一个特殊的对象Closure,这个对象就是闭包。它其实是一种特殊的对象。
函数的作用域是在代码的预解析阶段就确定好的,那么在这其中的闭包自然也是一样,它在我们编写代码的时候就已经确定好了。
作用域的作用就在于明确的告诉我们当前的上下文环境中那些变量可以参与程序的运行,在函数的执行上下文中,除了自身上下文能够直接访问的声明之外,还能从函数体的[[Scopes]]属性访问其他的作用域中的声明。
const a = 10
function fn() {
let b = 1
let c = 2
function foo() {
b = b + 1
const d = 30
return b + c + d
}
console.dir(foo)
return foo
}
fn()
我们可以看出,函数foo能访问到的声明,除了自身定义的d以外,还可以访问fn声明的变量b和c。最后还能访问Script对象和Global对象。
能访问自身变量d具体体现在Local对象中,而其他的则全部体现在函数的[[Scopes]]属性中,如下
而在其中的Glosure就是我们学习的闭包。
闭包就是一种特殊的对象,当函数A内部创建函数B,并且函数B访问函数A中声明的变量等声明时,闭包就会产生。比如上面函数fn内部创建了函数foo,并且在foo中访问了fn声明的b和c,此时就会出现一个闭包。闭包是基于作用域产生的,让函数内部可以访问到函数外部的声明。
对于函数foo来说,闭包的引用存在于自身的[[Scopes]]属性中,也就是说,只要函数体foo存在,闭包就会持久存在。而如果函数体被回收,闭包对象同样会被回收。
通过前面的函数调用栈我们知道,在预解析阶段,函数声明会创建一个函数体,并在代码中持久存在,但不是所有的函数体都可以持久存在。上面的例子就是这样的。函数fn的函数体能在内存中持续存在,原因是fn在全局上下文中声明,fn的应用始终存在,因此我们始终能访问到fn。而函数foo则不同,函数foo是在函数fn的执行上下文中声明,当执行上下文执行完成,执行上下文会被回收,在fn执行上下文中的函数foo也会被回收,如果不做特殊处理那么闭包也会被回收。
将上面的例子调整一下:
const a = 10
function fn() {
let b = 1
let c = 2
function foo() {
b = b + 1
console.log(b)
const d = 30
return b + c + d
}
console.dir(foo)
return foo
}
fn()()
fn()()
fn()()
分析一下执行过程,当fn执行后,会创建函数体foo,并作为fn的返回值。当fn执行完成后,则对应的执行上下文会被回收,此时foo作为fn的执行上下文的一部分,也会被回收。那么保存在foo的[[Scopes]]的闭包对象,自然也会被回收。
因此,多次执行fn()()的结果,实际是在创建多个不同的fn执行上下文,中间的foo创建的闭包对象,始终都没有保存下来,会随着fn的上下文一起被回收。因此,多次创建的不同闭包对象也不会保留下来,相互之间不会影响。
而当我们使用一些方法保留函数foo的引用,情况就会发生变化,
const a = 10
function fn() {
let b = 1
let c = 2
function foo() {
b = b + 1
console.log(b)
const d = 30
return b + c + d
}
console.dir(foo)
return foo
}
const bar = fn()
bar()
bar()
bar()
在代码中,我们使用新的变量保持了fn函数内部foo的引用,也就是说,即使fn执行完成且上下文回收,但是由于函数foo有bar的保存,那么即使foo是fn的上下文,也不会回收,而是会持续存在。因此bar的多次执行其实执行的是同一个函数体,所以函数体foo的闭包对象也是同一个,当foo修改b的值时,就会出现累加的效果,因为他们在修改同样的对象。
总结一下:
- 闭包的产生只需要在函数内部声明函数,并且内部函数访问上层函数作用域中的声明就会产生闭包。
- 闭包是一种特殊的对象,保存在函数体的[[Scopes]]属性中。
- 闭包对象在代码解析阶段,根据词法作用域产生
- 闭包对象不是不能回收,需要视情况确定