先记住这个答案
闭包本身不会导致泄漏。垃圾回收的规则是:从全局对象和调用栈出发,还能找到的对象就留着,找不到的才回收。闭包引用了外层变量,所以只要闭包本身还能被找到,这些变量也得留着。真正的问题是闭包被 addEventListener、setInterval、全局缓存这类长期存在的东西拿住了,而业务上早就不需要它。避免的办法是在组件销毁或功能结束时解除注册,并让闭包只引用它真正需要的那一小块数据。
- 垃圾回收看的是能不能从根对象找到,不看函数有没有执行完
- 泄漏的源头是长期持有闭包的监听器、定时器、全局缓存
- 修法是解除注册,光把局部变量设成
null往往没用 - 同一作用域里的闭包共享一个环境,一个引用大对象,全都留着
一个真实的泄漏:弹窗关了,监听器还在
最常见的场景是给 window 或 document 绑监听器,回调里用到了组件的数据。组件销毁了,但监听器挂在全局对象上,回调闭包还在,它引用的数据自然也回收不掉。
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 返回清理函数,也是同一个思路。
怎么证明确实泄漏了
内存涨了不一定是泄漏,缓存和当前页面状态本来就该留着。判断泄漏要看「重复同一个操作,内存能不能回到原来的水平」。
- 用 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 闭包互相引用就形成循环,谁都回收不掉。现代浏览器统一用标记清除,循环引用只要从根对象找不到就能回收。现在说的泄漏,基本都是被长期持有,不是循环引用。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。