前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
首页程序员面试题库闭包导致内存泄漏 如何避免
JavaScript函数与作用域

闭包为什么会导致内存泄漏?怎么避免?

闭包本身不是泄漏。问题出在闭包被全局对象、事件监听、定时器这类活得很久的东西拿着,它引用的外层变量就一直回收不掉。解决办法是在不用的时候解除这层持有,并且让闭包只引用真正需要的数据。

前端进阶之旅 · 一题精讲更新于 2026.09.29
JavaScript#函数与作用域
先看核心答案读代码示例
理解线索

一条引用链决定闭包能不能被回收

  1. 根对象全局对象、正在执行的调用栈,垃圾回收从这里开始找
  2. 长期持有者addEventListener、setInterval、全局 Map 拿着闭包
  3. 闭包环境闭包引用的外层变量,跟着闭包一起活着

V8 里同一个函数作用域创建的多个闭包共用一个上下文对象。哪怕某个闭包从不使用大对象,只要兄弟闭包用了,它也会把大对象一起留住。

核心回答

先记住这个答案

闭包本身不会导致泄漏。垃圾回收的规则是:从全局对象和调用栈出发,还能找到的对象就留着,找不到的才回收。闭包引用了外层变量,所以只要闭包本身还能被找到,这些变量也得留着。真正的问题是闭包被 addEventListener、setInterval、全局缓存这类长期存在的东西拿住了,而业务上早就不需要它。避免的办法是在组件销毁或功能结束时解除注册,并让闭包只引用它真正需要的那一小块数据。

  • 垃圾回收看的是能不能从根对象找到,不看函数有没有执行完
  • 泄漏的源头是长期持有闭包的监听器、定时器、全局缓存
  • 修法是解除注册,光把局部变量设成 null 往往没用
  • 同一作用域里的闭包共享一个环境,一个引用大对象,全都留着

一个真实的泄漏:弹窗关了,监听器还在

最常见的场景是给 window 或 document 绑监听器,回调里用到了组件的数据。组件销毁了,但监听器挂在全局对象上,回调闭包还在,它引用的数据自然也回收不掉。

注册时返回一个清理函数JavaScript
const listeners = new Set() // 模拟全局事件中心,它活得和页面一样久

function openDialog(pageData) {
  const userId = pageData.user.id // 只取需要的小值
  const onResize = () => console.log('resize', userId)
  listeners.add(onResize)
  return () => listeners.delete(onResize) // 交给调用方在关闭时执行
}

const close = openDialog({ user: { id: 7 }, bigList: new Array(10000).fill(0) })
console.log(listeners.size) // 弹窗打开,监听器在
close()
console.log(listeners.size) // 弹窗关闭,引用链断了
查看输出与解释
1
0

这里做了两件事。第一,回调只捕获了数字 userId,没有直接引用整个 pageData,所以 bigList 在 openDialog 返回后就可以回收。第二,注册时就返回清理函数,谁打开谁负责关闭。close() 执行后 Set 里不再有这个回调,回调和它的环境都会被回收。 真实代码里对应的就是 removeEventListener、clearInterval、observer.disconnect()、取消订阅。React 的 useEffect 返回清理函数,也是同一个思路。

容易被忽略的一种:兄弟闭包共享环境

很多人以为闭包只会留住自己用到的变量。大体没错,引擎会分析闭包到底引用了哪些变量。但有个细节:同一个函数作用域里创建的多个闭包,在 V8 里共用同一个上下文对象。

一个闭包用了大对象,另一个跟着遭殃
比如一个函数里先定义了 const big = new Array(1e6),再定义一个只在内部用一次的 log = () => console.log(big.length),最后返回一个完全不碰 big 的 tick 函数。直觉上 tick 不引用 big,big 应该能回收。实际上因为 log 引用了 big,big 被放进了这个作用域共享的上下文里,而 tick 也持有这个上下文,所以 big 会一直跟着 tick 活下去。 修法是把大对象的使用挪到单独的函数里,或者用完就把它置为 null。这里置 null 是有效的,因为切断的正是那条共享上下文里的引用。
什么时候置 null 没用
如果闭包还被全局的监听器列表拿着,把闭包内部某个变量设为 null 只能释放那一个变量,闭包本身和其他变量照样活着。先找到是谁在长期持有,把这层关系断开,才是根本的修法。

怎么证明确实泄漏了

内存涨了不一定是泄漏,缓存和当前页面状态本来就该留着。判断泄漏要看「重复同一个操作,内存能不能回到原来的水平」。

用 Heap snapshot 对比
在 Chrome DevTools 的 Memory 面板里,先拍一张快照,然后把「打开弹窗、关闭弹窗」重复做几次,点垃圾桶图标手动触发回收,再拍一张。对比两张快照,如果某类对象的数量随着操作次数线性增长,基本就是泄漏。选中这个对象,看下方的 Retainers 面板,它会列出从根对象到这个对象的引用路径,路径上通常能看到某个 listener 或 setInterval 的回调。
回答前,多想一步

容易答错的地方

用了闭包就会泄漏
闭包和普通对象一样受垃圾回收管理。一个函数返回的闭包,外面没人再引用它,它和它的环境就会被回收。只有闭包被持有的时间超过了业务需要,才算泄漏。
在销毁时把大对象设成 null 就够了
如果回调还挂在 window 上,闭包本身没被释放。设 null 只能处理掉一个变量,而且很容易漏。先 removeEventListener,这才是切断引用链。
用匿名函数注册,再用另一个匿名函数移除
removeEventListener('resize', () => {...}) 传进去的是一个新函数,和注册时那个不是同一个引用,什么都删不掉。注册时要把回调存到变量里,或者用 AbortController 的 signal 一次性移除。
试着用自己的话回答

面试官还会怎么问?

WeakMap 能解决闭包泄漏吗?

只能解决一部分。WeakMap 的键是弱引用,键对象在别处没人用了,这条记录会自动消失。适合「给 DOM 节点挂一些附加数据」这类场景。但如果闭包被全局数组直接持有,WeakMap 管不到那条引用链。

React 里最常见的闭包泄漏是什么?

useEffect 里注册了监听器、定时器或订阅,却没有返回清理函数。组件卸载后回调还在执行,还会去调用已卸载组件的 setState。修法就是在 useEffect 里返回一个函数,做对应的移除和清理。

老版本 IE 的闭包泄漏和现在是一回事吗?

不是一回事。老 IE 的 DOM 对象用引用计数管理,DOM 节点和 JS 闭包互相引用就形成循环,谁都回收不掉。现代浏览器统一用标记清除,循环引用只要从根对象找不到就能回收。现在说的泄漏,基本都是被长期持有,不是循环引用。

从一道题,走向一组知识

把知识连起来

函数与作用域

柯里化是怎么用闭包实现的?能手写一个 curry 吗?

柯里化靠闭包把前面传入的参数一直存着,中间函数被长期保存时同样会留住这些参数。

函数与作用域

函数声明和函数表达式有什么区别?为什么一个能提前调用一个不能?

回调常用函数表达式现场创建,每次创建都是一个新闭包,这也是 removeEventListener 删不掉匿名函数的原因。

参考资料

  • JavaScript:函数与作用域参考资料
  • Memory management - JavaScript | MDN

示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。

本题目录
  1. 先记住这个答案
  2. 一个真实的泄漏:弹窗关了,监听器还在
  3. 容易被忽略的一种:兄弟闭包共享环境
  4. 怎么证明确实泄漏了
  5. 容易答错的地方
  6. 面试官还会怎么问
  7. 把知识连起来
读懂,再试着讲出来

先看核心答案,再读代码。最后展开追问,检查自己有没有遗漏边界。

试着回答追问
浏览全部面试题理解原理,也关注真实的使用场景。回到顶部 ↑