先记住这个答案
闭包是函数与其定义时词法环境的组合。函数能够沿着定义时的作用域链访问外层变量,即使外层函数已经执行结束。它保留的是对变量绑定的访问能力,所以后续调用可以读到更新后的值,而不是创建时拍下的一张快照。
- 作用域看定义位置
- 保留变量绑定,不是值的副本
- 可达的引用决定内存生命周期
闭包的原理:函数和词法作用域
判断一个函数能访问哪些变量,要看它在源代码中定义在哪里。函数在别处执行,并不会改用调用位置的作用域。外层函数返回,只表示这次调用结束;仍被返回函数使用的词法绑定可以继续存在。
下面的 counter 和 anotherCounter 来自两次工厂调用,所以各有一份 count。counter 连续输出 1、2,说明它读写的是同一绑定;anotherCounter 输出 1,则说明不同调用之间没有共享这份状态。
function createCounter() {
let count = 0;
return function increment() {
count += 1;
return count;
};
}
const counter = createCounter();
const anotherCounter = createCounter();
console.log(counter());
console.log(counter());
console.log(anotherCounter());查看输出与解释
1
2
1第一次工厂调用对应的 count 变为 2,第二次调用创建的 count 只增加到 1。函数体相同,词法环境各自独立。
闭包的作用:把状态和行为放在一起
JS 闭包常用来保存函数需要跨调用复用的状态。关键不是“套了一层函数”,而是这份状态是否有清晰的归属和清理时机。
验证“JavaScript 闭包”中的“闭包的作用:把状态和行为放在一起”时,应固定输入、运行环境与触发顺序,并同时记录正常结果和失败结果。这样能确认本节结论来自目标机制,而不是缓存、旧状态或偶然时序。
- 封装私有状态
- 计数器、权限配置等只通过返回的方法访问,避免把可变状态挂到全局。多个实例需要隔离时,每次调用工厂创建一份环境。
- 保存定时器
- 防抖返回的函数共享一个 timer,每次输入清掉上次任务后重新安排。这是把闭包用于真实交互的典型例子。
- 缓存计算结果
- 记忆化函数用闭包保存 Map。缓存键的设计、容量上限与失效策略同样重要,不能只写“命中就返回”。
循环里的 var 和 let,为什么输出不同?
先不引入定时器,只把回调存进数组。这样能把“闭包到底读哪一个变量”和“回调什么时候执行”两个问题分开。var 版本里的回调共享一个 i;for 循环的 let 声明会为各次迭代建立独立绑定。
验证“JavaScript 闭包”中的“循环里的 var 和 let,为什么输出不同?”时,应固定输入、运行环境与触发顺序,并同时记录正常结果和失败结果。这样能确认本节结论来自目标机制,而不是缓存、旧状态或偶然时序。
const shared = [];
for (var i = 0; i < 3; i++) {
shared.push(() => i);
}
console.log(shared.map(fn => fn()).join(', '));
const separate = [];
for (let j = 0; j < 3; j++) {
separate.push(() => j);
}
console.log(separate.map(fn => fn()).join(', '));查看输出与解释
3, 3, 3
0, 1, 2第一个循环结束时共享的 i 已经是 3。第二个循环的三个回调各自访问对应迭代的 j,不会都读到循环结束后的值。
闭包与内存泄漏:问题在于不再需要的引用
保存状态本身就是闭包的用途,仍在使用的状态占内存不等于泄漏。需要排查的是:业务已经不需要某个对象,它却仍通过长期存活的监听器、定时器或缓存被引用。
例如组件注册了 window 事件监听,回调又引用了一批数据。组件卸载时应移除监听、取消未完成的定时任务,并按业务清理缓存。仅仅把某个局部变量赋为 null,不能保证其它引用也同时消失。定位问题时沿堆快照中的保留路径确认是谁在持有对象。
容易答错的地方
- “只有返回函数才叫闭包”
- 返回函数只是最容易观察的例子。传给定时器、事件监听器的回调也可能访问定义时的外层环境。排查“JavaScript 闭包”时还要核对输入、版本和执行顺序,并用反例确认修复后的边界。
- “闭包保存的是当时的值”
- 它访问变量绑定。多个闭包可以共享同一绑定,也可以来自不同工厂调用、访问各自独立的绑定。排查“JavaScript 闭包”时还要核对输入、版本和执行顺序,并用反例确认修复后的边界。
- “用了闭包就一定内存泄漏”
- 先看对象是否仍然可达、是否仍有业务用途。移除无用引用、控制缓存规模,比一概避免闭包更实际。
面试官还会怎么问?
面试时,怎样用 30 秒解释闭包是什么?
我会说:闭包让函数在外层调用结束后,仍然访问定义时的外层变量。比如创建计数器时,把 count 留在外层,返回的 increment 每次读写同一份 count。不同工厂调用产生不同环境,所以实例可以隔离;使用中要管理好监听器、定时器和缓存的生命周期。
两个闭包可以共享同一个变量吗?
可以。在同一次工厂调用里返回 increment 和 reset,两个函数都访问同一个 count,就共享了这份绑定。若希望相互隔离,要分别调用工厂创建环境,而不是复制同一个函数引用。
闭包和 this 有什么关系?
词法变量查找与 this 绑定是两个问题。普通函数的 this 通常由调用方式决定;箭头函数没有自己的 this,会沿外层环境查找。因此不能因为一个普通函数形成了闭包,就认为它也固定了外层 this。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。