Skip to content

1. 核心概念 (Core Concept)

闭包(Closure)是 JavaScript 中一个核心概念,它是指一个函数以及其周围状态(lexical environment,词法环境)的引用打包在一起的组合。换句话说,闭包允许我们将函数与该函数所运行的外部环境隔离开来。

2. 为什么需要它? (The "Why")

  1. 数据封装和私有变量 (Data Encapsulation and Private Variables): 闭包提供了一种机制来创建私有作用域,使得某些变量只能在特定的函数内部访问,从而实现数据的封装,避免全局污染或意外修改。
  2. 保持状态 (Maintaining State): 闭包可以“记住”创建它们时的环境,即使外部函数已经执行完毕,闭包内部仍能访问到外部函数作用域中的变量,从而在函数调用之间保持状态。
  3. 延迟执行和事件处理 (Deferred Execution and Event Handling): 在异步操作、事件处理或定时器中,常常需要函数在 future 的某个时间点执行,闭包可以确保这些函数在执行时能访问到当时正确的作用域变量。

3. API 与用法 (API & Usage)

闭包并非一个需要显式调用或特殊语法的 API,它是 JavaScript 语言中基于词法作用域的一种自然产物。当一个内部函数被“暴露”到其词法作用域之外时,闭包就形成了。

判断闭包的核心依据是: 一个函数是否能够记住并访问到在其词法作用域外部定义的变量,即使该外部作用域的函数已经执行完毕。

以下是一个经典的闭包示例:

javascript
function makeGreeter(greeting) {
  // 'greeting' 是 makeGreeter 的局部变量
  // 返回的匿名函数形成了一个闭包
  return function(name) {
    console.log(greeting + ', ' + name + '!');
  };
}

// 当 makeGreeter() 执行完毕后,它的作用域理论上应该销毁
const sayHello = makeGreeter('Hello');
const sayHey = makeGreeter('Hey');

// 但是,sayHello 和 sayHey 函数(闭包)
// 仍然能够访问到创建它们时 makeGreeter 的 'greeting' 变量
sayHello('Alice'); // 输出: Hello, Alice!
sayHey('Bob');   // 输出: Hey, Bob!

在这个例子中,sayHellosayHey 是由 makeGreeter 返回的内部函数。即使 makeGreeter 已经执行完成(其调用栈已弹出),但 sayHellosayHey 仍然可以访问到 makeGreeter 作用域内的 greeting 变量。这就是闭包的体现。

另一个常见的例子,用于解决循环中的异步问题:

javascript
// 错误示例:i 在定时器执行时已经是 5
// for (var i = 0; i < 5; i++) {
//   setTimeout(function() {
//     console.log(i); // 总是输出 5
//   }, i * 100);
// }

// 使用闭包解决 (以函数参数创建新作用域)
for (var i = 0; i < 5; i++) {
  (function(index) {
    setTimeout(function() {
      console.log(index); // 输出 0, 1, 2, 3, 4
    }, index * 100);
  })(i); // 将当前的 i 值作为参数传递给立即执行函数 (IIFE)
}

// 使用 let (块级作用域,自然形成闭包)
for (let j = 0; j < 5; j++) {
  setTimeout(function() {
    console.log(j); // 输出 0, 1, 2, 3, 4
  }, j * 100);
}

在这个循环示例中,通过立即执行函数(IIFE)创建新的作用域,将当前的 i 值“捕获”进去,从而使得定时器回调函数(闭包)能够访问到每次迭代时正确的 index 值。使用 let 声明则利用了其块级作用域的特性,每轮循环都会为 j 创建一个新的绑定,从而自然地解决了问题,这也体现了块级作用域与闭包的关系。

4. 关键注意事项 (Key Considerations)

  1. 内存开销 (Memory Overhead): 闭包会使其引用的外部变量一直保存在内存中,直到闭包本身被垃圾回收。如果闭包不再需要,应解除对它的引用,避免潜在的内存泄漏。
  2. “捕获”变量的引用 (Capturing Variable Reference): 闭包捕获的是外部变量的引用,而不是值的副本(基本类型在多数情况下看起来像副本是因为它们是按值传递的,但闭包记住的是变量本身)。如果外部变量的值在其被捕获后发生改变,闭包内部访问到的将是改变后的值(除非使用技巧如 IIFE 或 let 在每次迭代中创建新的变量绑定)。
  3. 判断的核心是词法作用域 (Judgment Based on Lexical Scope): 闭包的形成与函数的定义位置(词法作用域)相关,而不是执行位置。判断一个函数是否形成闭包,是看该函数是否访问了其定义位置所在的外部作用域的变量。
  4. JavaScript 中的常见现象 (Common Phenomenon in JS): 闭包在 JavaScript 中非常常见,几乎所有的回调函数、事件处理器、模块模式等等都广泛使用了闭包的特性。理解闭包对于理解 JavaScript 的运行机制至关重要。

5. 参考资料 (References)

不知道说啥了很无语了