JavaScript内存泄漏实际上并不复杂:本该被垃圾回收的内存,由于意外的强引用而无法释放,结果导致页面越来越卡,甚至直接崩溃。典型场景主要包括:意外全局变量、未清理的定时器、DOM引用残留、闭包偷偷持有大对象,以及模块静态引用不断堆积。

JavaScript 内存泄漏很少骤然发生,它更像一种慢性问题,由一些看似无害却长期持有引用的代码模式逐渐累积而成。要识别这些模式,关键在于理解核心逻辑:对象明明已经“不可达”,却依然被强引用着——垃圾回收器(GC)无法回收它,因为根(比如 window)或活跃对象的引用链上仍然挂着它。
意外创建的全局变量
首先来看最隐蔽也最常见的一类——意外全局变量。未声明变量、this 指向错误、严格模式缺失,都可能导致局部数据“逃逸”为全局属性。
- 直接赋值未声明变量:
function foo() { data = new Array(1e6); }→ 此时data变为了window.data,永远无法被回收 - 构造函数中
this赋值但未使用new调用:function User(name) { this.name = name; };如果误写为User('Alice'),则this指向window,导致name成为全局属性 - 解决办法:在整个项目中启用
'use strict';定期检查window或globalThis上是否存在异常增长的属性;使用 ESLint 规则no-implicit-globals进行拦截
定时器与回调长期存活
第二类常见模式,是定时器与回调长期存活。setInterval 或 setTimeout 的回调如果引用了外部大对象,而定时器又没有被清除,那个对象就会一直驻留在内存中。
- 典型场景:组件已经卸载,但
setInterval仍在轮询更新 DOM 或状态 - 风险点:回调内访问
this、闭包捕获props、state或大型数据结构 - 解决办法:组件销毁或页面离开前,显式调用
clearInterval(id)或clearTimeout(id);优先使用AbortController配合setTimeout的取消能力(现代方案)
DOM 引用残留与事件监听器未解绑
即便 DOM 元素已经被 remove() 或 innerHTML = '' 移除,只要 JS 代码仍然持有它的引用,或者监听器没有移除,它就无法被回收。这就像房子已经拆了,但钥匙还在你手里。
- 常见写法:
const el = document.getElementById('chart'); chartInstance = new Chart(el, ...);→ 后续执行el.remove()后,chartInstance仍强引用着el - 旧式监听器:
btn.addEventListener('click', handler)但未在合适时机调用btn.removeEventListener('click', handler) - 现代缓解:使用
once: true选项;将监听器绑定在父级进行事件委托处理;或用WeakMap存储关联数据,避免强引用
闭包不当持有大对象
闭包本身并不等于泄漏,这一点需要明确。但当内部函数长期存在——比如被导出、存入全局缓存、作为事件回调——同时又引用了外层函数中体积庞大的变量时,问题就出现了。
- 示例:
function createProcessor() { const bigData = fetchHugeList(); return () => console.log(bigData.length); };如果返回的函数被长期保留,bigData就会一直锁死 - 关键判断:看闭包函数是否“必须”访问那些大变量;能否采用延迟加载或按需计算的方式
- 解决办法:手动切断引用(如
processorRef = null);使用WeakRef+FinalizationRegistry(实验性,生产环境需谨慎);重构为工厂函数,每次调用生成独立的小闭包
模块级静态引用与单例滥用
最后来看模块级静态引用与单例滥用。ESM 模块的缓存机制使得顶层变量变成了“伪全局”,尤其在工具类、状态管理器、缓存实例中,容易越堆越多。
- 例子:
// cache.js中export const cache = new Map();多个页面导入后不断调用cache.set(key, hugeObj),却从不清理 - 风险延伸:
import的模块未被 GC 回收,其内部所有导出绑定和闭包都持续存活 - 解决办法:为缓存添加 LRU 或 TTL 机制;避免在模块顶层创建长生命周期对象;用函数封装代替模块级实例
识别这些模式不能仅靠猜测,而应借助工具验证。使用 Chrome DevTools 的 Memory 面板录制堆快照(Heap Snapshot),对比多次操作后的对象数量变化;重点关注 Detached DOM tree、Closure、Array、Object 实例的持续增长;再通过 “Retainers” 查看是谁在强引用它——通常能快速定位到那个忘记 clearInterval 的定时器,或者那个仍然持有 document.body 引用的闭包。
