面试的时候,面试官最喜欢问那种看似简单,但稍微一深挖就能暴露你功底的问题,比如“说说 JavaScript 中内存泄漏的几种情况?”你要是随便说点概念性的东西,可能直接被扣分。今天我们就来好好聊聊这个问题,挖一挖其中的坑和重点。
简单点说,内存泄漏就是某块内存本来应该被回收了,但由于某些原因,它还在占着地方。程序跑得时间越长,泄漏的内存越多,最终可能会拖垮系统。
虽然 JavaScript 有垃圾回收机制(GC),会自动管理内存,但它也有搞不定的场景。咱们得弄清楚这些“搞不定”的情况,才能从根本上避免。
JavaScript 的垃圾回收主要靠两种方法:标记清除和引用计数。
1、标记清除
这是最常用的方式。垃圾回收器会给所有变量打个“存活”标记,如果某个变量可以被代码访问到,那就保留;反之,标记为“死亡”,然后清除内存。
举个例子:
function example() {
let a = { x: 10 }; // a 被标记为存活
let b = { y: 20 }; // b 被标记为存活
b = null; // b 的引用被移除,标记为死亡
}
在 b = null
后,b
所占用的内存会被回收,因为没有任何地方再用到它。
2、引用计数
每个值的引用次数记录在案。如果某个值的引用次数变成 0,就意味着没人需要它了,这块内存就可以回收了。
例如:
let obj = { data: 'example' };
let ref = obj; // obj 的引用次数是 2
ref = null; // obj 的引用次数变成 1
obj = null; // obj 的引用次数变成 0,被回收
知道了基础原理,我们再看看 JS 中的几个常见内存泄漏情况。
1. 意外的全局变量
全局变量生命周期长,占用内存多,稍不留神就成了内存泄漏的元凶。
function badExample() {
hiddenGlobal = "Oops, I'm a global variable!";
}
上面这段代码中,hiddenGlobal
并没有通过 var
、let
或 const
声明,就直接成为了全局变量。如果你忘记清理它,它就会一直占用内存。
解决方法: 开启严格模式,强制要求变量声明。
"use strict";
function fixedExample() {
let hiddenGlobal = "Now I'm local!";
}
2. 定时器或回调未清理
定时器和回调函数如果没有及时清理,引用就会一直存在。
let someResource = getResource();
setInterval(() => {
console.log(someResource);
}, 1000);
如果 getResource()
获取的资源被释放了,但定时器没清理,someResource
依然会被引用,导致内存泄漏。
解决方法: 清除定时器。
let intervalId = setInterval(() => {
console.log("Running...");
}, 1000);
clearInterval(intervalId);
3. 闭包引起的内存泄漏
闭包会让函数内部变量一直存活,稍不注意,就可能导致内存泄漏。
function createClosure() {
let largeData = new Array(1000).fill('Leak!');
return () => {
console.log(largeData);
};
}
let closure = createClosure();
解决方法: 不再需要闭包时,解除对它的引用。
closure = null;
4. DOM 引用未清理
当 DOM 元素被删除,但代码中还有对它的引用时,内存同样无法被释放。
let node = document.getElementById('node');
document.body.removeChild(node);
// node 仍然有引用,不会被回收
解决方法: 在删除 DOM 时,清除相关引用。
node = null;
5. 事件监听未移除
注册事件监听器后,如果不在合适的时机移除,监听器会继续引用目标对象,导致内存泄漏。
let button = document.getElementById('button');
button.addEventListener('click', () => {
console.log('Clicked!');
});
// 即使删除了 button 节点,事件监听器仍然引用着它
document.body.removeChild(button);
解决方法: 使用 removeEventListener
取消事件监听。
button.removeEventListener('click', listenerFunction);
6. 循环引用
两个对象互相引用,会导致垃圾回收机制无法清理它们。
function createCycle() {
let obj1 = {};
let obj2 = {};
obj1.ref = obj2;
obj2.ref = obj1;
}
createCycle();
解决方法: 手动解除引用。
obj1.ref = null;
obj2.ref = null;
如果面试官问你“说说 JavaScript 中内存泄漏的几种情况”,你可以这样回答:
内存泄漏是指程序未能释放已经不再使用的内存。在 JavaScript 中,虽然有垃圾回收机制,但还是有几种常见的内存泄漏情况:
意外的全局变量:比如没有用 let
、const
或var
声明变量,导致全局引用无法被回收。定时器未清理: setInterval
或setTimeout
未清理会导致内存泄漏。闭包:闭包中引用的变量如果没有正确释放,也会造成泄漏。 DOM 引用未清理:删除 DOM 元素后,仍然存在对它的引用。 事件监听未移除:未及时用 removeEventListener
清理的事件监听会持续占用内存。循环引用:两个对象互相引用,导致无法回收。
要避免这些问题,我们可以通过严格模式、定时器清理、合理解除引用等方法来管理内存。比如,删除 DOM 时清除相关变量引用,或者在使用闭包时注意解除不再需要的引用。
目前,对编程、职场感兴趣的同学,大家可以联系我微信:golang404,拉你进入“程序员交流群”。
虎哥私藏精品 热门推荐 虎哥作为一名老码农,整理了全网最全《前端资料合集》。