一. “闭包”是个啥?
·通俗定义:
一个函数及其词法环境的统称,具有“记住”且能够访问其创建在词法作用域中变量的特性,即使函数在其原始作用域外被访问
本质就是上级作用域内变量的生命周期,因为被下级作用域内引用,而没有被释放。就导致上级作用域内的变量,等到下级作用域执行完以后才正常得到释放。
·简洁:
闭包 = 函数 + 被保存的创建时作用域
·代码示例:
var arr = []
for (var i = 1; i <= 5; i++) {
function foo(j) {
return function() {
console.log(j);
}
}
arr.push(foo(i))
}
// run
for (let n = 0; n < arr.length; n++) {
arr[n]()
}
其中,j变量就是拥有“记忆”的那个变量,是函数里封闭的变量
接下来让我们运行这段代码,看看运行结果是什么
给大家提供两个选项:
A.12345
B.66666
在给出答案之前我先给出另外一道题,仅在第一题代码的基础上修改部分
var arr = []
for (var i = 1; i <= 5; i++) {
arr.push(function() {
console.log(i);
})
}
// run
for (let n = 0; n < arr.length; n++) {
arr[n]()
}
同样给出两个选项
A.12345
B.66666
现在给出两题的答案:A B 或许会有些许疑问,但是现在从作用域上分析一下两道题
var arr = []
for (var i = 1; i <= 5; i++) {
arr.push(function() {
console.log(i);
})
}
// run
for (let n = 0; n < arr.length; n++) {
arr[n]()
}
在编译阶段
var使得arr,i变量提升进入全局作用域
arr被初始化为undifined
i被声明,同样初始化为undifined
在代码执行阶段
首先执行 arr = [] ,初始化数组
然后进入 for 循环,执行 i = 1 初始值设置
在循环体中,创建匿名函数并添加到 arr 数组,但这些函数并未执行
每次循环后, i 递增,直到 i > 5 时循环结束
结束循环时,i的值为6
而在//run的for循环运行arr时,由于需要前文中的i,此for循环无法检索便从内向全局作用域检索,此时i=6,故输出结果为66666
现在再看另一题的代码
var arr = []
for (var i = 1; i <= 5; i++) {
function foo(j) {
return function() {
console.log(j);
}
}
arr.push(foo(i))
}
// run
for (let n = 0; n < arr.length; n++) {
arr[n]()
}
在上一个的演示下,也分析一下这段代码
在编译阶段 var使得arr,i变量提升进入全局作用域
arr被初始化为undifined
i被声明,同样初始化为undifined
在代码执行阶段
首先执行 arr = [] ,初始化数组
然后进入 for 循环,执行 i = 1 初始值设置
在循环体中,创建匿名函数并添加到 arr 数组,但这些函数并未执行
与之前不同的是,j作为形参被赋予了值
在 i = 1 的 for 循环部分中,可视为 foo(j) /* foo(1) */ 被传入数组
而每个 j 都会存在一个执行上下文,这和上一题的 i 作用于全局执行上下文环境是不一样的
这也解释了为什么此题 j 会有多个参数的现象 , 而上一题 i 通过词法作用域访问的是已经变为 6 的原因
而这也就是闭包的体现,剩下的便是老生常谈的了
二.闭包是怎么形成的
1.作用域链(Scope Chain)是基础:解释JavaScript引擎在查找变量时,会先在当前作用域查找,找不到则根据外部作用域的引用(outer)向外层查找,这种链式关系就是作用域链
2.环境记录(Environment Record)与 [[OuterEnv]] :简要介绍执行上下文中的词法环境(LexicalEnvironment),其核心是一个环境记录,它记录了当前作用域内的标识符绑定。每个环境记录都有一个 [[OuterEnv]]内部属性,指向其外部环境记录,从而形成链式结构
3.函数对象的 [[Environment]]内部插槽:当一个函数被创建时,它会将当前执行上下文的词法环境引用保存在其内部的 [[Environment]]属性中。这就是函数能够“记住”其诞生地的关键
4.变量查找过程:当调用函数时,会创建新的执行上下文和词法环境。在查找变量时,引擎会从当前环境记录开始,顺着 [[OuterEnv]]构成的链向上查找,直到全局作用域([[OuterEnv]]为 null)
5.闭包的形成时机:结合以上几点,说明当内部函数引用了外部函数的变量,并且其生命周期(如被返回、被传递给其他函数、被全局变量引用)超出了外部函数的执行时间,就形成了闭包,外部函数的变量对象(或其所在的环境记录)不会被垃圾回收
三.闭包的优缺点
优点-变量私有化:能为函数引入私有变量,不会污染全局环境,也方便后续代码调试修改
缺点-内存泄漏:正如前文提及,闭包会创建执行上下文,而未被调用闭包函数时,数据无法被视为垃圾无法丢出栈。 而这不可避免的是内存大量被占用,因此在不需要闭包时应主动解除对闭包函数的引用