JavaScript 内存泄漏的核心是:不再使用的对象 / 资源,未被垃圾回收机制(GC)回收,长期占用内存,最终导致页面卡顿、性能下降。
以下是 前端开发中最常见、最容易踩坑 的内存泄漏写法(按高频优先级排序,结合实战代码 + 避坑方案,精准对应你之前学的定时器、闭包、DOM 等知识点)
一、高频内存泄漏写法(必避坑,每类配代码 + 原因)
1. 定时器未清除(最常见,对应你学的 setTimeout/setInterval)
这是前端最高频的内存泄漏!定时器(setTimeout/setInterval)只要未清除,其回调函数及内部引用的变量,会一直被浏览器持有,无法回收。
- 泄漏写法:
// 1. setInterval 未清除(无限执行,一直占用内存)
let count = 0;
setInterval(() => { // 未用 clearInterval 清除,页面卸载后仍执行
count++;
console.log(count);
}, 1000);
// 2. setTimeout 未清除(即使是一次性,未执行前也占用内存)
const timer = setTimeout(() => {
console.log('未清除的定时器');
}, 10000); // 延迟10秒,若页面提前卸载,定时器仍占用内存
// 忘记写:clearTimeout(timer)
- 泄漏原因:定时器回调函数被浏览器的 “定时器队列” 引用,只要队列中存在,回调及内部变量就无法被 GC 回收。
- 避坑方案:所有定时器必须手动清除(组件销毁、页面卸载时)
// 正确写法(React 示例,其他框架同理)
useEffect(() => {
const timer = setInterval(() => {}, 1000);
// 组件卸载时清除定时器
return () => clearInterval(timer);
}, []);
2. 闭包导致的内存泄漏(高频,尤其新手易踩)
闭包的核心是 “内部函数引用外部变量”,若闭包未被释放,外部变量会一直被持有,无法回收(即使外部函数已执行完)。
- 泄漏写法:
// 示例1:闭包引用外部变量,且闭包被长期持有
function outer() {
const bigArr = new Array(1000000).fill(1); // 大数组,占用大量内存
return function inner() { // 闭包:inner 引用 bigArr
console.log(bigArr.length);
};
}
const fn = outer(); // fn 持有 inner 闭包,bigArr 一直被引用,无法回收
// 即使不再使用 fn,也未手动释放:fn = null
- 泄漏原因:闭包(inner 函数)被外部变量(fn)引用,导致外部函数(outer)的局部变量(bigArr)无法被 GC 回收,长期占用内存。
- 避坑方案:闭包使用完后,手动释放引用(赋值为 null)
const fn = outer();
// 使用完闭包后,手动释放
fn = null; // 此时 inner 闭包被释放,bigArr 可被 GC 回收
3. DOM 元素引用未释放(高频,DOM 操作场景)
两种核心场景:① 移除 DOM 元素,但仍保留其 JS 引用;② 事件绑定未解绑,导致 DOM 与 JS 双向引用。
- 泄漏写法:
// 场景1:移除DOM,但JS仍引用它
const btn = document.getElementById('btn');
document.body.removeChild(btn); // DOM 已从页面移除
// 但 btn 变量仍引用该 DOM 元素,无法被 GC 回收
console.log(btn); // 仍能访问,说明未回收
// 场景2:事件绑定未解绑(DOM 移除后,事件仍持有引用)
const box = document.getElementById('box');
box.addEventListener('click', () => { // 绑定点击事件
console.log('点击');
});
document.body.removeChild(box); // 移除DOM,但事件未解绑
// 事件回调引用 box,box 引用 DOM,形成闭环,无法回收
- 泄漏原因:DOM 元素即使被移除,只要有 JS 变量(或事件)引用它,就无法被 GC 回收,形成 “无效 DOM 引用”。
- 避坑方案:① 移除 DOM 前,解绑事件;② 释放 DOM 引用(赋值为 null)
const box = document.getElementById('box');
const clickHandler = () => console.log('点击');
box.addEventListener('click', clickHandler);
// 正确写法:移除DOM前,解绑事件+释放引用
box.removeEventListener('click', clickHandler); // 解绑事件
document.body.removeChild(box); // 移除DOM
box = null; // 释放引用
4. 全局变量滥用(对应你学的 var/let/const)
未声明的变量、意外创建的全局变量,会挂载到 window(浏览器),一直占用内存(全局变量不会被 GC 自动回收,除非手动释放)。
- 泄漏写法:
// 1. 意外创建全局变量(未用 var/let/const 声明)
function fn() {
a = new Array(1000000).fill(1); // 未声明,a 成为全局变量(window.a)
}
fn();
// a 是全局变量,即使 fn 执行完,a 仍存在,无法回收
// 2. 全局变量引用大对象,未手动释放
let globalObj = { name: '张三', data: new Array(1000000).fill(1) };
// 不再使用 globalObj,但未释放
- 泄漏原因:全局变量的生命周期与页面一致,除非手动赋值为 null,否则会一直占用内存,尤其引用大对象时,泄漏更明显。
- 避坑方案:① 禁止意外创建全局变量(严格模式 use strict 可报错);② 全局变量使用完后,手动释放(赋值为 null)
'use strict'; // 严格模式,未声明变量会报错,避免意外全局变量
let globalObj = { data: bigArr };
// 使用完后释放
globalObj = null; // 大对象可被 GC 回收
5. 未释放的事件监听器 / 订阅(框架场景高频)
Vue/React 等框架中,未解绑的事件监听器、组件订阅(如 Vue 的 $on、React 的事件订阅),会导致组件销毁后,仍被引用,无法回收。
- 泄漏写法(Vue 示例):
export default {
mounted() {
// 订阅事件,但未在组件销毁时解绑
this.$bus.$on('test', this.handleTest); // 事件订阅
},
methods: {
handleTest() {
console.log('事件触发');
}
}
// 未写:beforeUnmount 中解绑事件
};
- 泄漏原因:组件销毁后,事件订阅仍存在,
this.handleTest被 $bus 引用,导致组件实例无法被 GC 回收。 - 避坑方案:组件销毁时,解绑所有事件订阅 / 监听器
export default {
mounted() {
this.$bus.$on('test', this.handleTest);
},
beforeUnmount() {
// 组件销毁时,解绑事件
this.$bus.$off('test', this.handleTest);
}
};
6. 其他常见泄漏写法(低频但易忽略)
- 控制台打印大对象:
console.log(bigArr),控制台会一直引用该对象,导致无法回收(开发环境可忽略,生产环境避免)。 - 未关闭的资源:WebSocket、AJAX 请求、Canvas 上下文等,未手动关闭,一直占用内存。
- 数组 / 对象无限累加:未清空的数组 / 对象,一直添加元素,导致内存占用越来越大(如全局数组一直 push 数据,未清空)。
二、核心总结(避坑口诀,好记好用)
- 定时器必清除(clearTimeout/clearInterval);
- 闭包用完置 null;
- DOM 移除先解绑(事件)再释放(引用);
- 全局变量少滥用,用完及时置 null;
- 事件订阅 / 资源,销毁时必关闭。
三、快速排查内存泄漏的小技巧(开发实用)
- 浏览器 DevTools(F12)→ Memory 面板:拍摄内存快照,对比两次快照,查看 “未被回收的对象”(如 Detached DOM 就是 DOM 泄漏)。

- 观察页面性能:页面长时间运行后,卡顿、加载变慢,大概率是内存泄漏(可通过 Performance 面板录制性能,查看内存占用趋势)。


被折叠的 条评论
为什么被折叠?



