以下是导致 JavaScript 内存泄漏的常见情况及具体示例:
一、全局变量引起的内存泄漏
1. 意外的全局变量
// 情况1:忘记声明变量
function createLeak() {
leak = 'I am a global variable'; // 没有var/let/const
}
// 情况2:在非严格模式下,this指向window
function badConstructor() {
this.property = 'I become global'; // this === window
}
badConstructor();
// 情况3:构造函数忘记使用new
function Person(name) {
this.name = name;
}
// 忘记new,this指向全局对象
const p = Person('John'); // 泄露:window.name = 'John'
2. 被遗忘的定时器和回调函数
// 情况1:未清理的setInterval
let data = getLargeData();
const intervalId = setInterval(() => {
// data一直存在,因为闭包引用
console.log(data.length);
}, 1000);
// 忘记清理
// clearInterval(intervalId); // 缺少这行
// 情况2:未清理的事件监听器
class Component {
constructor() {
this.data = new Array(1000000).fill('*');
this.handleClick = this.handleClick.bind(this);
document.addEventListener('click', this.handleClick);
}
handleClick() {
console.log('Clicked');
}
// 忘记在销毁时移除监听器
// destroy() {
// document.removeEventListener('click', this.handleClick);
// }
}
const comp = new Component();
// comp.destroy(); // 如果没有调用,data和handleClick都不会被释放
二、DOM 相关内存泄漏
1. 脱离 DOM 的引用
// 情况1:在JS中保留DOM引用,即使DOM已被移除
const elements = {
button: document.getElementById('myButton'),
image: document.getElementById('myImage')
};
// 从DOM中移除
document.body.removeChild(document.getElementById('myButton'));
// 但elements.button仍然引用着DOM元素
// 垃圾回收器无法回收这个button
// 应该:elements.button = null;
// 情况2:表格单元格引用问题
function createTable() {
const table = document.createElement('table');
const row = table.insertRow();
const cell = row.insertCell();
cell.textContent = 'Data';
// 某些浏览器中,单元格引用整行
const cellRef = cell; // 保留单元格引用
// 即使从DOM中移除表格
document.body.removeChild(table);
// cellRef仍然间接引用整个表格
return cellRef; // 导致整个表格无法被回收
}
2. 闭包陷阱
// 情况1:闭包持有大对象
function outer() {
const hugeArray = new Array(1000000).fill('*');
const hugeObject = { data: new Array(500000).fill('*') };
return function inner() {
// inner函数闭包引用了hugeArray和hugeObject
console.log('Hello');
// 即使inner不直接使用hugeArray,它仍然被保留
};
}
const closure = outer(); // hugeArray和hugeObject不会被释放
// 情况2:事件处理器中的闭包
function attachEvents() {
const element = document.getElementById('myElement');
const bigData = getHugeData();
element.addEventListener('click', function() {
// 这个匿名函数闭包引用了bigData
console.log(bigData.length);
});
// 即使移除元素,事件处理器中的闭包仍然持有bigData
// document.body.removeChild(element);
}
三、缓存不当导致的内存泄漏
// 情况1:无限增长的缓存
const cache = {};
function processData(key) {
if (!cache[key]) {
// 没有大小限制,缓存会无限增长
cache[key] = expensiveOperation(key);
}
return cache[key];
}
// 情况2:忘记清理的Map/Set
const activeRequests = new Map();
function makeRequest(url) {
const controller = new AbortController();
activeRequests.set(url, controller);
fetch(url, { signal: controller.signal })
.then(response => {
// 处理响应后忘记清理
// activeRequests.delete(url); // 缺少这行
})
.catch(() => {
// activeRequests.delete(url); // 缺少这行
});
}
// 情况3:对象属性作为缓存
function DataProcessor() {
this.cache = {}; // 实例属性作为缓存
}
DataProcessor.prototype.process = function(key) {
if (!this.cache[key]) {
this.cache[key] = computeExpensiveValue(key);
}
return this.cache[key];
};
// 创建多个实例,每个都有缓存
const processors = Array(1000).fill().map(() => new DataProcessor());
四、循环引用问题
// 情况1:DOM与JS对象的循环引用
function createCircularReference() {
const element = document.getElementById('someElement');
const obj = {};
obj.element = element; // JS对象引用DOM
element.objRef = obj; // DOM引用JS对象
// 即使从DOM中移除元素
document.body.removeChild(element);
// 在某些浏览器(特别是旧版IE)中,循环引用会导致内存泄漏
// 现代浏览器通常能处理,但最好手动解除
// element.objRef = null;
}
// 情况2:对象间的循环引用
function createObjectCycle() {
const objA = { name: 'A' };
const objB = { name: 'B' };
objA.ref = objB; // A引用B
objB.ref = objA; // B引用A
// 如果objA和objB不再被其他代码引用,现代GC能回收
// 但如果有外部引用其中一个,两者都无法回收
return objA; // 只返回objA,但objB也无法被回收
}
五、被遗忘的订阅/观察者
// 情况1:Pub/Sub模式中的订阅
class EventBus {
constructor() {
this.subscribers = [];
}
subscribe(callback) {
this.subscribers.push(callback);
return () => {
const index = this.subscribers.indexOf(callback);
if (index > -1) this.subscribers.splice(index, 1);
};
}
}
const bus = new EventBus();
const bigData = new Array(1000000).fill('data');
// 订阅
const unsubscribe = bus.subscribe(() => {
console.log(bigData.length); // 闭包引用bigData
});
// 忘记取消订阅
// 组件销毁时应该调用:unsubscribe();
// 情况2:ResizeObserver/MutationObserver
function setupObserver() {
const element = document.getElementById('observed');
const data = getLargeData();
const observer = new ResizeObserver((entries) => {
// 闭包引用data
console.log(data.length, element.offsetWidth);
});
observer.observe(element);
// 元素被移除时忘记disconnect
// element.parentNode.removeChild(element);
// observer.disconnect(); // 缺少这行
}
六、Web API 相关泄漏
// 情况1:未关闭的WebSocket
let socket = new WebSocket('ws://example.com');
socket.onmessage = function(event) {
const largeData = JSON.parse(event.data);
processData(largeData);
};
// 页面离开时忘记关闭
// window.addEventListener('beforeunload', () => {
// socket.close(); // 缺少这行
// });
// 情况2:未释放的Web Workers
const worker = new Worker('worker.js');
worker.postMessage({ command: 'start', data: largeArrayBuffer });
// 忘记终止worker
// worker.terminate(); // 缺少这行
// 情况3:未清除的FileReader
const reader = new FileReader();
reader.onload = function(e) {
const largeText = e.target.result;
// 处理大文件内容
};
reader.readAsText(largeFile);
// reader对象会一直存在,直到被覆盖或页面卸载
七、框架特定的内存泄漏
React 示例:
// 情况1:未清理的副作用
import { useEffect, useState } from 'react';
function LeakyComponent() {
const [data, setData] = useState(null);
useEffect(() => {
// 异步操作,组件卸载后可能还在执行
fetchData().then(result => {
setData(result); // 组件卸载后设置状态
});
// 缺少清理函数
// return () => {
// // 取消请求或设置标志位
// };
}, []);
return <div>{data}</div>;
}
// 情况2:事件监听器未移除
function EventListenerLeak() {
useEffect(() => {
const handleResize = () => {
console.log(window.innerWidth);
};
window.addEventListener('resize', handleResize);
// 忘记移除
// return () => window.removeEventListener('resize', handleResize);
}, []);
return <div>Content</div>;
}
Vue 示例:
// 情况1:全局事件总线未清理
export default {
mounted() {
// 全局事件监听
EventBus.$on('some-event', this.handleEvent);
},
beforeDestroy() {
// 忘记移除
// EventBus.$off('some-event', this.handleEvent);
}
};
// 情况2:第三方库实例未清理
export default {
data() {
return {
chart: null
};
},
mounted() {
this.chart = new Chart(this.$refs.canvas, {
// 大量配置和数据
});
},
beforeDestroy() {
// 忘记销毁
// this.chart.destroy();
// this.chart = null;
}
};
八、Console 引起的泄漏
// 开发中常见但易忽略的问题
function processUserData(user) {
const processed = expensiveProcessing(user);
// ❌ 调试代码留在生产环境
console.log(processed); // 控制台保持对象引用
// ❌ 将大对象记录到控制台
// console.log(largeArray); // 控制台可能保留完整快照
return processed;
}
// 某些情况下,控制台会阻止垃圾回收
const hugeObject = { data: new Array(1000000).fill('x') };
console.log('Object:', hugeObject); // 控制台持有引用
hugeObject = null; // 但控制台仍然可以访问它
九、检测和预防内存泄漏的工具
1. Chrome DevTools
- Memory面板:拍摄堆快照,比较前后差异
- Performance面板:记录内存分配时间线
- Allocation instrumentation:跟踪内存分配
2. 代码检查工具
// 使用WeakMap/WeakSet避免强引用
const weakCache = new WeakMap(); // 键是弱引用
// 使用WeakRef和FinalizationRegistry
const registry = new FinalizationRegistry((heldValue) => {
console.log(`${heldValue} 被回收了`);
});
const obj = { data: 'large' };
const weakRef = new WeakRef(obj);
registry.register(obj, 'myObject');
// 当obj被回收时,会触发回调
3. 监控代码
// 定期检查内存使用
function monitorMemory() {
if (window.performance && performance.memory) {
const used = performance.memory.usedJSHeapSize;
const limit = performance.memory.jsHeapSizeLimit;
const percent = (used / limit * 100).toFixed(2);
if (percent > 80) {
console.warn(`内存使用率过高: ${percent}%`);
}
}
}
setInterval(monitorMemory, 30000);
十、最佳实践总结
- 及时清理引用:不再使用的对象设为
null - 使用弱引用:
WeakMap、WeakSet、WeakRef - 清理事件监听器:
removeEventListener、disconnect - 清理定时器:
clearInterval、clearTimeout - 注意闭包:避免不必要的大对象被闭包捕获
- 框架生命周期:在组件销毁时清理资源
- 避免全局缓存:为缓存设置大小限制和过期策略
- 生产环境移除console:使用构建工具自动移除
- 代码审查:定期检查常见的内存泄漏模式
- 性能测试:使用自动化测试检测内存泄漏
内存泄漏通常不会立即显现,但会随着时间积累导致应用变慢或崩溃。通过理解这些常见模式并采用预防措施,可以显著减少内存泄漏问题。