什么是闭包与词法作用域
闭包让函数能够持续访问外层作用域中的变量。词法作用域决定变量在何处可见,依据的是源代码的嵌套结构,而不是运行时的调用顺序。
当一个函数引用了外层函数作用域中的变量时,就形成了闭包。只要闭包仍然存在,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 提供的封装替代方案