全局变量意外持有闭包引用是隐蔽性强、复现难但后果严重的内存泄漏源头;它不依赖DOM或定时器,仅一次赋值就可能使整个作用域链长期驻留内存。

全局变量意外持有闭包引用,是 JavaScript 中隐蔽性强、复现难但后果严重的内存泄漏源头。它不依赖 DOM 或定时器,仅靠一次赋值就可能让整个作用域链长期驻留内存。
识别关键信号:从现象反推问题
当出现以下组合特征时,应高度怀疑全局变量 + 闭包的隐式绑定:
-
内存快照中 Closure 类型持续增多,且多数 retainers 指向
window、globalThis或某个长生命周期模块对象(如AppManager) - 堆中 Detached DOM tree 数量稳定,但 Closure 占用内存却阶梯式上涨——说明泄漏与 DOM 无关,更可能是纯 JS 引用链
-
执行组件卸载、页面跳转后,某些闭包仍保留在内存中,且其内部可访问到已销毁组件的
this.state、大型 props 对象或缓存数据 -
使用
console.memory观察:totalJSHeapSize持续上升,而gc.get_count()(若在 Node.js 环境)或performance.memory显示活跃对象未减少
定位具体代码位置的实操步骤
不是靠猜,而是用工具链锁定源头:
- 在 Chrome DevTools 的 Memory 面板 中连续拍三张堆快照(操作前、操作中、操作后),切换到 Comparison 视图,筛选类型为
Closure,按Retained Size降序排列 - 点击可疑闭包 → 查看右侧 Retainers 标签页 → 若路径显示
window.xxxHandler或globalThis.cacheMap→ 基本确认是全局变量挂载所致 - 回到 Sources 面板,搜索该全局变量名(如
window.handler或globalThis.tempFn),定位赋值语句;检查右侧是否用了箭头函数或匿名函数,且该函数内部访问了外层大对象(如hugeData、document.body) - 启用 Console 的 “Preserve log”,在疑似赋值处加
console.trace(),确认调用栈是否来自初始化逻辑、错误捕获回调或未清理的模块导出
典型错误模式与修复对照
这些写法看着无害,实则埋雷:
-
错误:
const init = () => { const bigObj = new Array(100000).fill(0); window.cleanup = () => console.log(bigObj.length); };
→ 闭包捕获了整个bigObj,而window.cleanup让它永生 -
修复:改用显式传参,避免隐式捕获:
window.cleanup = (len) => console.log(len); init(); window.cleanup(bigObj.length); -
错误:
var handler = function() { doSomethingWith(config); }; window.addEventListener('online', handler);
→config是全局配置对象,体积大;handler被挂到window上,无法被 GC -
修复:移出全局作用域,或使用
WeakRef包裹非关键引用:const configRef = new WeakRef(config); window.addEventListener('online', () => { const c = configRef.deref(); if (c) doSomethingWith(c); });
预防性编码习惯
从源头切断风险比事后排查更高效:
- 禁用非严格模式下的隐式全局变量——始终开启
"use strict",让未声明赋值直接报错 - 避免在模块顶层或 IIFE 外直接给
window/globalThis赋值函数或对象;必须暴露时,优先用export或事件总线通信 - 对需长期持有的函数引用,检查其闭包内容:用
console.dir(fn)在控制台展开,观察[[Scopes]]中是否包含预期外的大对象或 DOM 节点 - 构建阶段加入 ESLint 规则,如
no-global-assign和自定义规则检测window\.|globalThis\.后紧跟函数字面量

















