说起内存管理,可能是很多同学既熟悉、又陌生的一个领域。熟悉是因为我们经常会听到这个词,陌生是因为好像我们在开发时又很少去关注它。
不过,在目前的前端面试中,内存管理的问题被问的又比较频繁。所以,咱们今天就来看看 JS 内存管理的那些事...
JavaScript 如何管理内存
JavaScript 与许多现代编程语言一样,使用自动内存管理,即:JS 会自动完成垃圾回收。这也意味着,我们在程序运行时无需手动分配和释放内存(这也是很多同学很少关注它的原因)。
自动内存管理(垃圾回收)主要通过 标记-清除 法完成,大致分为两个阶段:
- 标记阶段:垃圾收集器从根对象开始,标记所有可到达或可访问的对象。
- 清除阶段:任何未标记(即可无法访问)的对象都被视为“垃圾”,并且其内存将被释放。
但是,自动的内存管理毕竟无法解决所有场景下的内存问题,所以在某些特殊情况下就会导致出现 内存泄漏 的问题。
而这个 内存泄漏 就是我们在面试中聊到内存管理时的,重点描述 内容!
关于【内存泄漏】
垃圾回收经常会伴随着内存泄漏的概念。不过我们需要知道的是 内存泄漏 和 内存溢出 是不同的。
PS 内存溢出指的是:程序运行过程中需要使用的内存大于系统能够提供的内存。即:系统内存不足。
所谓的内存泄漏指的是:当一块内存(通常是变量)未被释放(垃圾回收),同时我们也无法访问到它时。大家可以简单理解为:一个变量,我们访问不到它了,但是它占用的内容还没有被回收。那么此时就会出现内存泄漏的问题:
1. 未清理的定时器或回调
- 场景:使用 setInterval 或 setTimeout,但在组件销毁或不再需要时没有清理。
- 结果:定时器依然存在,引用被保留,导致无法释放内存。
- 示例:
function startTimer() {
setInterval(() => {
console.log('...');
}, 1000);
}
// 如果调用此函数后不清理定时器,则会造成内存泄漏
- 修复:
const timer = setInterval(() => {
console.log('Timer running...');
}, 1000);
clearInterval(timer); // 组件销毁时清理
2. DOM 引用未释放
- 场景:保留对已移除 DOM 元素的引用。
- 结果:虽然 DOM 节点从页面上被删除,但 JavaScript 中仍有对它的引用,导致内存泄漏。
- 示例:
let element = document.getElementById('leak');
document.body.removeChild(element);
// 仍然引用 element,导致内存无法释放
- 修复:
element = null; // 手动清除引用
3. 闭包中的多余引用
- 场景:闭包内的变量保留了对外部对象的引用,导致对象无法被垃圾回收。
- 结果:长时间的引用链阻止了内存的释放。
- 示例:
function createClosure() {
const largeObject = new Array(1000).fill('leak');
return function() {
console.log(largeObject);
};
}
const closure = createClosure();
// 即使 closure 不再使用,largeObject 依然存在
- 修复:
function createClosure() {
let largeObject = new Array(1000).fill('leak');
return function() {
console.log(largeObject);
largeObject = null; // 主动清除引用
};
}
4. 全局变量或未声明变量
- 场景:未使用 var, let, 或 const 定义变量,导致变量挂载在全局作用域。
- 结果:全局变量生命周期与页面一致,无法被垃圾回收。
- 示例:
function leak() {
leakedVariable = 'I am a leak'; // 隐式全局变量
}
leak();
- 修复:
function noLeak() {
let localVariable = 'I am safe'; // 使用局部作用域变量
}
5. 事件监听器未清理
- 场景:在 DOM 元素上绑定了事件监听器,但没有在元素销毁时移除。
- 结果:监听器仍然存在,阻止 DOM 元素被回收。
- 示例:
const button = document.getElementById('myButton');
button.addEventListener('click', () => console.log('点击!'));
document.body.removeChild(button);
// 内存泄漏:button 和监听器仍被引用
- 修复:
const button = document.getElementById('myButton');
const handler = () => console.log('点击!');
button.addEventListener('click', handler);
button.removeEventListener('click', handler); // 组件销毁时移除
document.body.removeChild(button);
6. 忘记清理 Map/Set 中的键或值
- 场景:Map 或 Set 中的键/值对象未手动移除。
- 结果:即使这些对象不再需要,也会因其被引用而无法回收。
- 示例:
const map = new Map();
const obj = { key: 'value' };
map.set(obj, 'data');
// 即使 obj 不再使用,map 仍然引用它
- 修复:
map.delete(obj); // 手动清理不需要的键
7. 闭环引用
- 场景:两个对象互相引用,导致垃圾回收无法判断它们是否可释放。
- 结果:循环引用造成内存泄漏。
- 示例:
function CircularReference() {
const obj1 = {};
const obj2 = {};
obj1.ref = obj2;
obj2.ref = obj1;
}
- 修复:
// 如果可能,避免互相引用,或通过 WeakMap/WeakSet 管理对象
const obj1 = new WeakMap();
const obj2 = {};
obj1.set(obj2, 'value');