《JavaScript闭包:从表象到本质的深入探索》

93 阅读4分钟

一. “闭包”是个啥?

·通俗定义:

一个函数及其词法环境的统称,具有“记住”且能够访问其创建在词法作用域中变量的特性,即使函数在其原始作用域外被访问

本质就是上级作用域内变量的生命周期,因为被下级作用域内引用,而没有被释放。就导致上级作用域内的变量,等到下级作用域执行完以后才正常得到释放。

·简洁:

闭包 = 函数 + 被保存的创建时作用域

·代码示例:

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使得arri变量提升进入全局作用域

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使得arri变量提升进入全局作用域

arr被初始化为undifined

i被声明,同样初始化为undifined

在代码执行阶段

首先执行 arr = [] ,初始化数组

然后进入 for 循环,执行 i = 1 初始值设置

在循环体中,创建匿名函数并添加到 arr 数组,但这些函数并未执行

与之前不同的是,j作为形参被赋予了值

i = 1for 循环部分中,可视为 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.闭包的形成时机:结合以上几点,说明当内部函数引用了外部函数的变量,并且其生命周期(如被返回、被传递给其他函数、被全局变量引用)超出了外部函数的执行时间,就形成了闭包,外部函数的变量对象(或其所在的环境记录)不会被垃圾回收


三.闭包的优缺点

优点-变量私有化:能为函数引入私有变量,不会污染全局环境,也方便后续代码调试修改

缺点-内存泄漏:正如前文提及,闭包会创建执行上下文,而未被调用闭包函数时,数据无法被视为垃圾无法丢出栈。 而这不可避免的是内存大量被占用,因此在不需要闭包时应主动解除对闭包函数的引用