面试官:说说 JavaScript 中内存泄漏的几种情况?

文摘   2024-11-23 10:07   山西  

面试的时候,面试官最喜欢问那种看似简单,但稍微一深挖就能暴露你功底的问题,比如“说说 JavaScript 中内存泄漏的几种情况?”你要是随便说点概念性的东西,可能直接被扣分。今天我们就来好好聊聊这个问题,挖一挖其中的坑和重点。

简单点说,内存泄漏就是某块内存本来应该被回收了,但由于某些原因,它还在占着地方。程序跑得时间越长,泄漏的内存越多,最终可能会拖垮系统。

虽然 JavaScript 有垃圾回收机制(GC),会自动管理内存,但它也有搞不定的场景。咱们得弄清楚这些“搞不定”的情况,才能从根本上避免。

JavaScript 的垃圾回收主要靠两种方法:标记清除和引用计数。

1、标记清除
这是最常用的方式。垃圾回收器会给所有变量打个“存活”标记,如果某个变量可以被代码访问到,那就保留;反之,标记为“死亡”,然后清除内存。

举个例子:

function example({
    let a = { x10 }; // a 被标记为存活
    let b = { y20 }; // 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 并没有通过 varletconst 声明,就直接成为了全局变量。如果你忘记清理它,它就会一直占用内存。

解决方法: 开启严格模式,强制要求变量声明。

"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 中,虽然有垃圾回收机制,但还是有几种常见的内存泄漏情况:

  1. 意外的全局变量:比如没有用 letconstvar 声明变量,导致全局引用无法被回收。
  2. 定时器未清理setIntervalsetTimeout 未清理会导致内存泄漏。
  3. 闭包:闭包中引用的变量如果没有正确释放,也会造成泄漏。
  4. DOM 引用未清理:删除 DOM 元素后,仍然存在对它的引用。
  5. 事件监听未移除:未及时用 removeEventListener 清理的事件监听会持续占用内存。
  6. 循环引用:两个对象互相引用,导致无法回收。

要避免这些问题,我们可以通过严格模式、定时器清理、合理解除引用等方法来管理内存。比如,删除 DOM 时清除相关变量引用,或者在使用闭包时注意解除不再需要的引用。

目前,对编程、职场感兴趣的同学,大家可以联系我微信:golang404,拉你进入“程序员交流群”。

虎哥私藏精品 热门推荐

虎哥作为一名老码农,整理了全网最前端资料合集

资料包含了《前端面试题PDF合集》、《前端学习视频》、《前端项目及源码》,总量高达108GB。

全部免费领取全面满足各个阶段程序员的学习需求!

web前端专栏
回复 javascript,获取前端面试题。分享前端教程,AI编程,AI工具,Tailwind CSS,Tailwind组件,javascript教程,webstorm教程,html教程,css教程,nodejs教程,vue教程。
 最新文章