← 中文资源

简体中文版本

闭包与词法作用域

闭包是从外层词法作用域捕获变量的函数,即使外层函数已经返回,仍能访问这些变量。本文解释作用域链、模块模式、循环捕获陷阱与内存注意事项。

阅读英文原文

什么是闭包与词法作用域

闭包让函数能够持续访问外层作用域中的变量。词法作用域决定变量在何处可见,依据的是源代码的嵌套结构,而不是运行时的调用顺序。

当一个函数引用了外层函数作用域中的变量时,就形成了闭包。只要闭包仍然存在,JavaScript 引擎就会在内存中保留外层函数的变量环境,即使外层函数已经执行完毕。闭包“关闭”并捕获了它引用的外层变量,并在自身的整个生命周期内维持这些绑定。

词法作用域如何工作

JavaScript 在函数定义的位置沿着作用域链向上解析变量引用,而不是在函数被调用的位置:

const greeting = 'global';

function outer() {
  const greeting = 'outer';

  function inner() {
    console.log(greeting); // "outer" —— 在定义位置解析
  }

  return inner;
}

const fn = outer();
fn(); // "outer",而不是 "global"

闭包的实际应用

模块模式

const counter = (function() {
  let privateCount = 0; // 被闭包捕获

  return {
    increment: () => ++privateCount,
    getCount: () => privateCount,
  };
})();

counter.increment();
console.log(counter.getCount()); // 1
// privateCount 无法从外部直接访问

函数工厂

function createMultiplier(multiplier) {
  return function(x) {
    return x * multiplier; // 捕获 multiplier
  };
}

const double = createMultiplier(2);
const triple = createMultiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15

常见陷阱:循环变量捕获

// ❌ 错误:所有回调都输出 5
for (var i = 0; i < 5; i++) {
  setTimeout(() => console.log(i), 100);
}

// ✅ 修复:使用 let(每次迭代都有独立的块级作用域)
for (let i = 0; i < 5; i++) {
  setTimeout(() => console.log(i), 100); // 0,1,2,3,4
}

常见误解

  • 闭包在创建时复制变量值。 实际上闭包捕获的是活的引用,因此对被捕获变量的修改在闭包内部可见。
  • 只有箭头函数会形成闭包。 所有 JavaScript 函数都会在其词法环境中形成闭包。
  • 闭包默认会导致内存泄漏。 闭包只会阻止被捕获变量被回收;如果闭包本身不再可达,这些变量仍会被释放。

何时使用闭包

  • 封装私有状态(模块模式)
  • 从通用函数创建专用函数(柯里化、部分应用)
  • 在异步回调之间保留配置
  • 实现记忆化缓存

内存注意事项

闭包会把被捕获的变量保留在内存中。如果闭包生命周期很长,应避免捕获不再需要的大对象;使用完毕后将其置为 null。

相关概念

  • 词法作用域:静态作用域解析
  • 垃圾回收:被捕获变量何时可以被回收
  • 私有类字段:现代 JavaScript 提供的封装替代方案