跳到主要内容

闭包(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

解释要点:

  • incmakeCounter 里面创建,所以它的作用域链包含 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/数组但从不删除。

结论:不是闭包错,是“引用生命周期”没管住。


易错点/坑

  • 把“闭包”理解成“函数返回函数”:这是常见写法,但闭包的关键是“捕获词法环境”,不等价于特定形式。
  • 以为闭包一定慢:现代引擎优化很多,真正的性能问题通常是“捕获了不该捕获的大对象”或“热路径频繁创建闭包”。

速记要点(可背诵)

  • 闭包 = 函数 + 创建时的词法环境。
  • 是否回收看可达性:被引用就不回收。
  • 泄漏多来自事件/定时器/缓存导致的长期引用。