🧑💻 面试官:闭包是什么?
🙋♂️ 我:函数能够访问它外层作用域中的变量。
🧑💻 面试官:外层函数已经返回,变量为什么还能访问?
🙋♂️ 我:返回的函数仍然保留了对相应环境的引用。
🧑💻 面试官:那闭包一定会内存泄漏吗?页面已经关掉了某个面板,面板的数据却还占着内存,你会沿着什么去查?
闭包的重点是「引用还在不在」。函数结束不等于相关数据必须回收,继续可达也不等于一定发生了泄漏。
面试速答(60 秒版)
闭包是函数与它所处词法环境的组合,使函数能够访问定义位置外层的变量。外层函数返回以后,只要相关函数及环境仍然可达,需要的变量就可能继续存在。
闭包不是把当时所有变量都拍成一份快照。多个函数可以共享同一个变量,读取时看到后续修改;不同工厂调用又可以形成独立状态。
这本身是正常能力,常用于封装状态和回调。内存泄漏的问题,是不再需要的数据仍被长期引用。例如全局事件监听器或计时器保留了回调,回调又保留面板数据。
因此,排查时要看引用链,并在生命周期结束时取消监听、计时器和订阅,而不是见到闭包就改写。

知识点详解:函数返回了,为什么还能记住计数?
先看一个能运行的例子
下面是同一个计数器的 TS 和 Python 写法。两者都能说明“返回的函数保留了访问外层状态的能力”,但不代表两种语言的内存实现完全相同。
function makeCounter() {
let count = 0;
return () => {
count += 1;
return count;
};
}
const a = makeCounter();
const b = makeCounter();
console.log(a(), a(), b()); // 1、2、1def make_counter():
count = 0
def increase():
nonlocal count
count += 1
return count
return increase
a = make_counter()
b = make_counter()
print(a(), a(), b()) # 1 2 1以 TS 这段代码为例,调用 makeCounter() 时创建 count 和内部函数。外层返回后,a 仍然指向内部函数,内部函数需要访问那次调用中的 count,所以这份状态仍然能使用。
第二次调用创建了另一份状态,b 不会继承 a 已经增加的数字。MDN 的闭包指南解释了函数与词法环境的关系。
它记住的是变量关系,不是冻结的旧值
如果两个内部函数共同访问同一次调用里的 count,一个增加,另一个读取,就会看到增加后的值。
因此,说“闭包捕获变量”通常比说“复制变量值”更准确。对于具体语言和写法,还要看作用域规则。
常见的循环题也是这样:var 不创建每次迭代独立的块级绑定,多个回调可能读到同一个最后值;用 let 时,每次迭代会有对应绑定,结果就不同。
不要把这种题背成“闭包总能保存当时的值”,因为它恰好考的是多个回调有没有共享同一个变量。
正常保留和泄漏,差在哪里?
计数器还在使用,count 保留下来是必要的状态。没有它,计数功能就不能继续。
但假设页面有一个大型图表面板,创建时注册了 window 上的事件监听器。面板关闭后,监听器没有移除,回调还在,回调所需的图表数据也可能仍在。
从垃圾回收的角度,它仍然可达;从产品的角度,它已经不该继续占用。这才是泄漏要关注的关系。
也不是外层定义过的所有对象都一定被完整保留。引擎可能优化,具体要看实际引用、代码与分析工具,不能靠源码里“在同一作用域”就计算内存占用。

清理要清掉长期保留的入口
移除监听器时,需要使用对应的事件类型、回调引用和相关选项。重新写一个长得一样的箭头函数,不是原来的回调,通常移除不了。
计时器需要取消,订阅需要退订。只把局部变量设成 null,如果长期入口仍然持有真正的数据或另一个引用,也解决不了问题。
实际排查可以反复打开、关闭面板,观察堆快照中的 retained size 与 retaining path。看是谁仍然保留它,再判断那条引用是否应该存在。
这样得到的是能验证的清理方案,而不是“闭包会泄漏,所以不用闭包”的结论。
面试官继续追问
有循环引用,就一定回收不了吗?
现代 JavaScript 垃圾回收通常能够处理不可达的循环。关键是它是否仍然从根对象可达,不是两个对象是否互相指着。
把回调注册到按钮上,按钮删除了就一定没问题吗?
要看还有谁引用按钮、回调和相关数据。如果按钮与监听关系整体已不可达,可以被回收;如果全局注册表仍持有它们,就可能继续留下。
闭包和全局变量,哪个更好?
闭包可以把状态限制在需要它的函数组中,减少全局暴露。但仍然要管理生命周期。封装方式不同,不代表自动获得零泄漏保证。
面试速记卡
- 闭包:函数保留对定义位置相关词法环境的访问。
- 状态:同一环境可以共享,不同调用可以独立。
- 回收:看是否可达,不看外层函数是否已经返回。
- 泄漏:已经不需要的数据,仍被长期入口保留。
- 清理:取消监听、计时器和订阅,检查真实引用链。
