JavaScript 闭包是什么?为什么变量没有被释放,什么时候会内存泄漏?

JavaScript 闭包是什么?为什么变量没有被释放,什么时候会内存泄漏?

闭包是函数与它所处词法环境的组合,使函数能够访问定义位置外层的变量。外层函数返回以后,只要相关函数及环境仍然可达,需要的变量就可能继续存在。

闭包不是把当时所有变量都拍成一份快照。多个函数可以共享同一个变量,读取时看到后续修改;不同工厂调用又可以形成独立状态。

这本身是正常能力,常用于封装状态和回调。内存泄漏的问题,是不再需要的数据仍被长期引用。例如全局事件监听器或计时器保留了回调,回调又保留面板数据。

因此,排查时要看引用链,并在生命周期结束时取消监听、计时器和订阅,而不是见到闭包就改写。

函数返回了,为什么还能记住计数?

先看一个能运行的例子

下面是同一个计数器的 TS 和 Python 写法。两者都能说明"返回的函数保留了访问外层状态的能力",但不代表两种语言的内存实现完全相同。

TypeScript
typescript 复制代码
function makeCounter() {
  let count = 0;
  return () => {
    count += 1;
    return count;
  };
}

const a = makeCounter();
const b = makeCounter();
console.log(a(), a(), b()); // 1、2、1
Python
python 复制代码
def 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 垃圾回收通常能够处理不可达的循环。关键是它是否仍然从根对象可达,不是两个对象是否互相指着。

把回调注册到按钮上,按钮删除了就一定没问题吗?

要看还有谁引用按钮、回调和相关数据。如果按钮与监听关系整体已不可达,可以被回收;如果全局注册表仍持有它们,就可能继续留下。

闭包和全局变量,哪个更好?

闭包可以把状态限制在需要它的函数组中,减少全局暴露。但仍然要管理生命周期。封装方式不同,不代表自动获得零泄漏保证。

面试速记卡

  • 闭包,函数保留对定义位置相关词法环境的访问。
  • 状态,同一环境可以共享,不同调用可以独立。
  • 回收,看是否可达,不看外层函数是否已经返回。
  • 泄漏,已经不需要的数据,仍被长期入口保留。
  • 清理,取消监听、计时器和订阅,检查真实引用链。

我是程序员Sunday。本文收录于 sunday面试指南,原文与配图可继续阅读。

相关推荐
JarvanMo41 分钟前
Flutter 终于有自己的 React Flow 了,vyuh_node_flow 上手实录
前端
狂师1 小时前
爆火!新一代e2e AI测试框架,单日狂揽1500 star
前端·人工智能·测试
恋猫de小郭1 小时前
Flitz ,一个把 Flutter 玩成热插拔 Preview 的有趣项目,不错的思路
android·前端·flutter
闭包不眠1 小时前
Word转HTML后体积不对,裁掉的截图原图还在
前端·图像处理·人工智能·计算机视觉·html·word
言乐61 小时前
PythonLOLM自动生成复盘报告模型
开发语言·前端·数据库·人工智能·python
tryCbest1 小时前
第二篇:CSS3 全部知识点全解 + 项目样式实现
前端·css·css3
一木 之林1 小时前
Agent智能体学习总结:从一次调用到模型自主规划、工具调用与 ReAct 循环的项目实战-6/6
前端·人工智能·学习·react.js
程序员大阳2 小时前
副队长CSS教程(36)–移动布局与响应式
前端·css·响应式·媒体查询·移动布局
程序员Sunday2 小时前
React 的 useEffect 怎么工作?依赖数组、清理函数和重复执行如何理解?
前端·javascript