在 JavaScript 中,内存泄漏通常发生在不需要的内存没有被垃圾回收器释放时。以下是常见的几种情况:
1. 意外的全局变量
// 意外的全局变量
function foo() {bar = "这是一个全局变量"; // 没有使用 var/let/const
}function baz() {this.accidentalGlobal = "这也是全局变量"; // 在非严格模式下,this 指向全局对象
}
baz();
解决方案:
// 使用严格模式
"use strict";function foo() {let bar = "局部变量"; // 使用 let/const
}
2. 被遗忘的定时器和回调函数
// 未清理的定时器
let data = getData();
setInterval(() => {let node = document.getElementById('Node');if(node) {node.innerHTML = JSON.stringify(data);}
}, 1000);// 未移除的事件监听器
const button = document.getElementById('button');
button.addEventListener('click', onClick);// 页面卸载时没有移除监听器
解决方案:
// 清理定时器
const intervalId = setInterval(callback, 1000);
// 需要时清理
clearInterval(intervalId);// 移除事件监听器
const button = document.getElementById('button');
button.addEventListener('click', onClick);
// 需要时移除
button.removeEventListener('click', onClick);
3. DOM 引用
// 保留对 DOM 元素的引用
let elements = {button: document.getElementById('button'),image: document.getElementById('image')
};// 即使从 DOM 中移除了元素,仍然在内存中保留引用
function removeButton() {document.body.removeChild(document.getElementById('button'));// elements.button 仍然引用着已移除的 DOM 元素
}
解决方案:
// 及时清理引用
function cleanUp() {elements.button = null;elements.image = null;
}
4. 闭包
// 闭包导致的内存泄漏
function createClosure() {let largeArray = new Array(1000000).fill('*');return function() {console.log(largeArray.length);// largeArray 一直被闭包引用,无法被回收};
}const closure = createClosure();
解决方案:
// 及时释放闭包引用
function useClosure() {const closure = createClosure();// 使用完毕后释放closure = null;
}
5. 缓存对象
// 无限增长的缓存
const cache = {};
function setCache(key, value) {cache[key] = value;
}// 没有清理机制,缓存会无限增长
解决方案:
// 使用有大小限制的缓存
class LimitedCache {constructor(maxSize = 100) {this.maxSize = maxSize;this.cache = new Map();}set(key, value) {if (this.cache.size >= this.maxSize) {const firstKey = this.cache.keys().next().value;this.cache.delete(firstKey);}this.cache.set(key, value);}
}
6. 分离的 DOM 节点
// 从 DOM 树中移除但仍在 JavaScript 中引用的节点
let detachedTree;
function create() {const ul = document.createElement('ul');for(let i = 0; i < 10; i++) {const li = document.createElement('li');ul.appendChild(li);}detachedTree = ul; // 保留引用但未添加到 DOM
}create();
// detachedTree 引用的整个 UL 树都无法被回收
7. 事件监听器在组件销毁时未移除
// 在单页应用中常见的问题
class Component {constructor() {this.handleResize = this.handleResize.bind(this);window.addEventListener('resize', this.handleResize);}handleResize() {// 处理逻辑}// 缺少销毁方法,事件监听器会一直存在
}// 正确的做法
class SafeComponent {constructor() {this.handleResize = this.handleResize.bind(this);window.addEventListener('resize', this.handleResize);}destroy() {window.removeEventListener('resize', this.handleResize);}
}
预防内存泄漏的最佳实践
- 使用严格模式防止意外的全局变量
- 及时清理定时器和事件监听器
- 避免不必要的全局变量
- 在组件销毁时清理所有引用
- 使用弱引用(WeakMap、WeakSet)当需要时
- 定期进行内存分析使用开发者工具
// 使用 WeakMap 避免内存泄漏
const weakMap = new WeakMap();
let domNode = document.getElementById('node');
weakMap.set(domNode, 'some data');// 当 domNode 被移除时,WeakMap 中的条目会自动被垃圾回收
通过遵循这些实践,可以显著减少 JavaScript 应用中的内存泄漏问题。