菜单

闭包

相关源文件

本文档解释了闭包这一核心 JavaScript 概念,它是 JavaScript 最强大和最基本的功能之一。闭包对于理解函数作用域如何运作、数据如何在函数调用之间保留以及许多 JavaScript 模式(如模块)如何实现至关重要。有关模块模式的详细信息,请参阅模块模式

什么是闭包?

闭包是一种行为,它发生在函数访问其自身作用域之外的变量时,即使在通常无法访问这些变量的作用域中执行也是如此。换句话说,函数“闭合(closes over)”其外部词法环境中的变量,即使在这些环境执行完毕后,仍保留对它们的访问。

要观察到闭包,必须满足以下三个条件

  • 必须涉及一个函数
  • 函数必须引用来自外部作用域的至少一个变量
  • 函数必须在作用域链中与变量(或多个变量)不同的分支中被调用

在这个例子中,内部的 greetStudent() 函数“闭合(closes over)”了其外部作用域中的 students 数组和 studentID 参数。即使 lookupStudent() 执行完毕,这些变量仍然对返回的函数可访问。

来源: scope-closures/ch7.md19-68

闭包如何工作

要理解闭包,我们需要理解 JavaScript 的词法作用域在编译和执行期间是如何工作的。

闭包可视化

每次 lookupStudent() 运行时,都会创建一个新的作用域,其中包含它自己的 students 数组和 studentID 参数。当函数执行完毕时,这些变量并没有被垃圾回收,而是通过闭包被返回的 greetStudent() 函数保持引用,从而保留在内存中。

来源: scope-closures/ch7.md131-133 scope-closures/ch7.md142-149

一个常见的误解是闭包捕获了函数创建时变量的值。实际上,闭包维护的是变量本身的实时链接

在这里,increment() 闭合了 count 变量,而不仅仅是它的初始值。每次调用都会更新同一个变量。

来源: scope-closures/ch7.md134-220

闭包的常见用途

1. 数据隐私和封装

闭包提供了一种创建只能由特定函数访问的私有变量的方法

count 变量无法从返回的对象的外部方法访问,从而提供了封装。

来源: scope-closures/ch7.md296-337 scope-closures/ch6.md72-138

2. 回调函数和事件处理程序

闭包是回调函数和事件处理程序工作方式的基础

即使在 setupButton() 执行完毕后,事件处理函数仍然通过闭包保持对 message 变量的访问。

来源: scope-closures/ch7.md296-337

3. 函数工厂

闭包能够创建专门化的函数

每个返回的函数都闭合了其自身唯一的 multiplier 值。

来源: scope-closures/ch7.md109-133

闭包的生命周期与内存

闭包直接影响变量的生命周期。通常在其作用域完成后会被垃圾回收的变量,只要存在一个闭合它的函数,它就会一直保持活跃。

意识到这种内存保留很重要。如果你创建了许多闭合大型数据结构的函数,并且没有释放这些函数引用,这可能会导致内存泄漏。

来源: scope-closures/ch7.md456-461 scope-closures/ch7.md519-526

按变量 vs. 按作用域

尽管从技术上讲,闭包在内部是按作用域发生的,但从概念上讲,将其视为按变量发生会更有用。函数只维护对其实际使用的外部变量的引用,而不是外部作用域中的所有变量。

来源: scope-closures/ch7.md519-526

闭包常见的陷阱

循环变量与闭包

一个常见的错误是假设闭包捕获的是变量的当前值。例如

所有三个函数都闭合了同一个 i 变量,当它们被调用时,i 的最终值为 3

为了解决这个问题,我们可以为每次迭代创建一个新的作用域

在循环中使用 let 会为每次迭代创建 i 的新绑定。

来源: scope-closures/ch7.md221-295

性能考量

尽管闭包功能强大,但它们确实伴随着内存方面的考虑

  1. 内存使用:闭合的变量只要闭合函数存在,就会保留在内存中。
  2. 垃圾回收:变量只有在其所有闭合它们的函数本身不再被引用时,才符合垃圾回收的条件。
  3. 谨慎取消订阅:对于长期运行的应用程序,当不再需要事件处理程序和其他回调引用时,务必明确地将其移除。

来源: scope-closures/ch7.md456-518

实际示例:使用闭包创建计算器

闭包的一个实际示例是创建能在操作之间保持状态的计算器

calculator() 函数在 total 变量周围创建了一个闭包,允许返回的方法共享和操作此状态。

来源: scope-closures/apB.md152-272

闭包 vs. 对象属性

闭包和对象属性都可以用于存储状态,但它们有不同的特性

闭包对象属性
默认私有默认公共
只能通过闭合它们的函数访问任何引用该对象的代码都可访问
无法直接检查或枚举属性可以被列出和检查
通过词法作用域实现封装封装需要额外的模式

当您希望确保某些数据只能以受控方式访问和修改时,闭包尤其有价值。

来源: scope-closures/ch8.md98-164 scope-closures/ch7.md373-419

结论

闭包是 JavaScript 最强大的特性之一。它们允许函数保持与其词法环境中变量的连接,即使在其他地方执行也是如此。这使得数据隐私、有状态函数和模块模式等模式成为可能。

理解闭包对于编写高效的 JavaScript 至关重要,因为它们出现在语言的各个方面——从回调函数和事件处理程序到更高级的模式,如柯里化和模块。