闭包(Closure)是什么?有什么用?会造成内存泄漏吗?
面试速答(30 秒版 TL;DR)
- 闭包:函数和其“创建时的词法环境(lexical environment)”绑定在一起,即使函数在别处执行,也能访问当时作用域里的变量。
- 本质:JS 是词法作用域语言,变量解析沿着作用域链向外查找;闭包让外层变量在函数返回后仍保持可达。
- 常见用途:封装私有变量、工厂函数、函数柯里化、回调里保留上下文、模块化(IIFE 时代)等。
- 是否会内存泄漏:闭包本身不是泄漏;泄漏通常来自“无意间长期持有引用”(比如把闭包挂在全局、事件监听不解绑、定时器不清理)。
心智模型:作用域链 + 可达性
闭包不是“神奇的语法”,而是两条规则叠加的结果:
- 词法作用域:变量能否访问由代码写在哪里决定,不由调用位置决定。
- GC 可达性:只要还有引用能到达某个对象/环境,它就不会被回收。
最小例子:闭包如何“保留变量”
function makeCounter() {
let n = 0;
return function inc() {
n += 1;
return n;
};
}
const c = makeCounter();
c(); // 1
c(); // 2
解释要点:
inc在makeCounter里面创建,所以它的作用域链包含n。makeCounter返回后,n仍被inc引用,因此不会被回收。
典型题 & 标准答法
Q1:闭包和作用域链是什么关系?
答法要点:
- 作用域链决定“变量从哪找”。
- 闭包让“外层作用域的变量在函数返回后仍可访问”,因为引用仍然存在。
Q2:循环里用 var 为什么常出 bug?怎么修?
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// 3 3 3
原因:var 是函数作用域,三个回调共享同一个 i。
修法 1:用 let(块级作用域)
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// 0 1 2
修法 2:IIFE 捕获(了解即可)
for (var i = 0; i < 3; i++) {
((x) => setTimeout(() => console.log(x), 0))(i);
}
闭包引发“泄漏”的常见场景
- 事件监听不解绑:回调闭包引用了大对象或 DOM,导致长期可达。
- 定时器未清理:
setInterval持续引用回调及其捕获变量。 - 全局缓存无限增长:把闭包/数据塞进 Map/数组但从不删除。
结论:不是闭包错,是“引用生命周期”没管住。
易错点/坑
- 把“闭包”理解成“函数返回函数”:这是常见写法,但闭包的关键是“捕获词法环境”,不等价于特定形式。
- 以为闭包一定慢:现代引擎优化很多,真正的性能问题通常是“捕获了不该捕获的大对象”或“热路径频繁创建闭包”。
速记要点(可背诵)
- 闭包 = 函数 + 创建时的词法环境。
- 是否回收看可达性:被引用就不回收。
- 泄漏多来自事件/定时器/缓存导致的长期引用。