MST

星途 面试题库

面试题:JavaScript垃圾回收机制下原始类型与引用类型的资源管理

JavaScript有自己的垃圾回收机制,原始类型和引用类型在这种机制下,资源管理方面有哪些不同?在复杂应用场景中,如何避免因原始类型或引用类型资源管理不当而导致的内存泄漏问题?请结合实际案例详细说明。
41.5万 热度难度
编程语言JavaScript

知识考点

AI 面试

面试题答案

一键面试

原始类型和引用类型在垃圾回收机制下资源管理的不同

  1. 存储方式
    • 原始类型:包括 stringnumberbooleannullundefinedsymbol(ES6 新增),它们的值是直接存储在栈(stack)中的简单数据段,占据固定大小的空间。例如:
let num = 10;

这里的 num 直接在栈中存储值 10

  • 引用类型:如 ObjectArrayFunction 等,它们的值是存储在堆(heap)中的对象。在栈中存储的是一个指向堆中实际对象的引用地址。例如:
let obj = {name: 'John'};

栈中 obj 存储的是指向堆中 {name: 'John'} 这个对象的引用。 2. 垃圾回收机制

  • 原始类型:由于它们在栈中存储,当它们超出作用域时,栈会自动清理相关数据,不需要垃圾回收机制主动干预。例如:
function test() {
    let num = 10;
}
test();
// 函数执行完毕,num 超出作用域,栈自动清理 num 的数据
  • 引用类型:垃圾回收机制通过标记清除算法或引用计数算法来管理它们的内存。标记清除算法会定期标记所有从根对象(如全局对象)可达的对象,然后清除那些未被标记的对象(即不可达对象)。引用计数算法则是跟踪每个对象被引用的次数,当引用次数为 0 时,回收该对象的内存。但引用计数算法存在循环引用的问题,例如:
function circularReference() {
    let obj1 = {};
    let obj2 = {};
    obj1.a = obj2;
    obj2.a = obj1;
}
circularReference();
// 这里 obj1 和 obj2 相互引用,在引用计数算法下,即使函数执行完毕,它们引用次数都不为 0,导致内存泄漏

在现代 JavaScript 引擎中,主要采用标记清除算法来避免循环引用导致的内存泄漏问题。

在复杂应用场景中避免内存泄漏的方法及实际案例

  1. 避免原始类型导致的内存泄漏
    • 合理使用闭包:闭包可能会导致原始类型数据在不该存在的时候依然存在于内存中。例如:
function outer() {
    let num = 10;
    return function inner() {
        console.log(num);
    };
}
let closure = outer();
// 这里闭包 inner 引用了 outer 作用域中的 num,即使 outer 执行完毕,num 依然不能被清理
// 避免方法:如果不再需要闭包对 num 的引用,可以手动将闭包设为 null
closure = null;
  1. 避免引用类型导致的内存泄漏
    • 事件监听移除:在 DOM 操作中,给元素添加事件监听器后,如果在元素被移除时没有移除事件监听器,会导致内存泄漏。例如:
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF - 8">
    <title>内存泄漏示例</title>
</head>

<body>
    <button id="btn">点击</button>
    <script>
        let btn = document.getElementById('btn');
        let data = {message: '一些数据'};
        btn.addEventListener('click', function () {
            console.log(data.message);
        });
        // 假设之后要移除按钮
        document.body.removeChild(btn);
        // 这里虽然按钮从 DOM 中移除,但事件监听器依然引用着 data,导致 data 不能被回收
        // 正确做法是移除事件监听器
        btn.removeEventListener('click', function () {
            console.log(data.message);
        });
        document.body.removeChild(btn);
    </script>
</body>

</html>
  • 解除循环引用:如前面提到的循环引用案例,在不需要对象相互引用时,手动解除引用。例如:
function circularReference() {
    let obj1 = {};
    let obj2 = {};
    obj1.a = obj2;
    obj2.a = obj1;
    // 解除循环引用
    obj1.a = null;
    obj2.a = null;
}
circularReference();
// 这样在函数执行完毕后,obj1 和 obj2 可以被垃圾回收机制回收
  • 避免不合理的全局变量:全局变量在整个应用生命周期内都存在,如果不合理使用,可能导致内存泄漏。例如:
// 错误做法
function add() {
    window.result = 1 + 2;
}
add();
// 这里在全局对象 window 上添加了 result 变量,即使 add 函数执行完毕,result 依然存在于内存中
// 正确做法是使用局部变量
function add() {
    let result = 1 + 2;
    return result;
}
let res = add();