js内存泄漏本质是不该存在的引用链阻止对象回收:①全局变量未声明导致挂载window/global;②事件监听器未解绑;③闭包长期持有大对象;④定时器/websocket未清理。

直接上干货:JS内存泄漏不是玄学,是可定位、可复现、可删代码解决的具体问题。核心就一条——不该存在的引用链,让本该回收的对象活了下来。下面从最常踩的坑开始,讲清楚现象、原理和动手改法。
全局变量意外挂载
没加 let/const 的变量会自动变成 window(浏览器)或 global(Node)属性,永不释放。
典型写法:
function loadData() {
cacheData = fetchBigList(); // 漏了声明关键字
}
后果:每次调用都往全局塞新数据,内存只增不减。
修复方式:
- 全项目启用
'use strict',让未声明变量直接报错 - 用 ESLint 规则
no-implicit-globals拦截 - 把缓存逻辑封装进模块级
const cache = new Map(),并配清理策略
事件监听器没解绑
DOM 元素被移除后,如果监听器还在,回调函数及其闭包里的所有变量都会卡在内存里。
常见场景:
将一个或多个视频上传至YouTube。当用户想“上传到YouTube”“发YouTube”“批量上传”“upload to YouTube”“post videos to YouTube”或发布到YouTube时使用此技能。
- SPA 页面切换时,组件销毁但
addEventListener没调removeEventListener - 用了
this.handleClick.bind(this)却没保存绑定后的函数引用,导致无法精准解绑
更稳妥的写法:
element.addEventListener('click', handler, { once: true }); // 一次性事件
// 或在组件卸载时:
element.removeEventListener('click', handler);
闭包持有大对象不释放
闭包本身不是问题,问题是它被长期持有,连带把本该回收的大数组、大对象一起锁住。
例子:
function createProcessor() {
const bigBuffer = new ArrayBuffer(10 * 1024 * 1024); // 10MB
return () => console.log('running');
}
const proc = createProcessor(); // bigBuffer 现在被闭包锁住
修复关键点:
- 确认闭包是否真需要访问那个大对象;不需要就提前置空:
bigBuffer = null - 把大资源从闭包作用域中“摘出去”,改用参数传入或按需加载
- 用
WeakMap存储关联数据,避免强引用拖住对象生命周期
定时器与 WebSocket 长期运行不清理
一个没 clearInterval 的心跳,每秒新增一个闭包引用;WebSocket 重连逻辑里反复创建新监听器却不销毁旧的,都是渐进式泄漏。
自查要点:
- 所有
setInterval必须配对clearInterval,且确保执行时机可靠(比如在组件unmount或连接关闭时) - WebSocket 实例销毁前,调用
ws.removeEventListener清理所有监听器 - 避免在定时器回调里直接用
this或箭头函数捕获外部大对象










