JS哪些行为会造成“内存泄漏”

48 阅读4分钟
  1. 内存泄漏的定义

    • 内存泄漏是指程序在申请内存后,无法释放已申请的内存空间,一次次的内存申请和未释放内存会导致内存消耗越来越大,最终可能会使程序崩溃或者系统变慢。
    • 在 JavaScript 中,由于其自动垃圾回收机制,可能不像一些没有自动内存管理的语言(如 C/C++)那样频繁地出现内存泄漏,但内存泄漏的情况还是可能出现。
  2. JavaScript 中会导致内存泄漏的情况及举例

    • 未解除的引用

      • 背景 :JavaScript 的垃圾回收机制并不是立即回收内存。当一个对象没有任何引用指向它时,垃圾回收器才会在适当的时机回收它所占用的内存。如果程序中存在未解除的引用,就会导致内存泄漏。

      • 举例

        • 假设我们有一个全局对象,它引用了另一个对象。
        const globalObj = {};
        function createObj() {
            const obj = {
                largeData:new Array(1000000).join('*') // 创建一个比较大的数据
            };
            globalObj.target = obj; // 全局对象引用了 obj
            return obj;
        }
        const myObj = createObj();
        // 后续代码
        myObj = null; // 试图释放 myObj 的引用
        // 此时,虽然 myObj 被设置为 null,但 globalObj.target 仍然引用着原来的 obj 对象
        // 如果程序中没有后续对 globalObj.target 的处理,就会导致内存泄漏
        
        • 在这个例子中,即使我们将 myObj 设置为 null,但由于 globalObj.target 仍然引用着 obj,垃圾回收器不会回收 obj 所占用的内存,从而造成了内存泄漏。
    • 闭包中的内存泄漏

      • 背景 :闭包可以使得内层函数访问外层函数的作用域。如果处理不当,闭包可能会导致外层函数的作用域中的变量一直被内层函数引用,从而无法被垃圾回收。

      • 举例

        • 假设有一个定时器函数使用了闭包。
        function setupTimeout() {
            const largeData = new Array(1000000).join('*'); // 创建一个比较大的数据
            setTimeout(function() {
                console.log('Timeout executed');
                // 这里的闭包函数引用了 largeData
                // 如果没有其他操作,当这个定时器执行完毕后,largeData 应该被回收
                // 但由于闭包机制,largeData 可能一直被引用着
            }, 1000);
        }
        setupTimeout();
        // 在这个例子中,理论上 setTimeout 执行结束后,largeData 应该被回收
        // 但由于 JavaScript 引擎的实现等因素,可能会导致 largeData 长时间无法被回收,造成内存泄漏
        
        • 在这个例子中,setTimeout 中的匿名函数(闭包)引用了外层函数 setupTimeout 中的 largeData。虽然理论上在定时器执行完成后,largeData 应该可以被回收,但在实际的 JavaScript 引擎实现中,可能会因为闭包对变量的引用而使 largeData 无法及时被回收,从而导致内存泄漏。
    • DOM 事件监听器未移除

      • 背景 :当给 DOM 元素添加事件监听器后,如果在元素被销毁或者页面结构发生变化(如元素被移除)时没有移除事件监听器,那么和事件监听器相关的函数和变量就无法被垃圾回收,从而导致内存泄漏。

      • 举例

        • 假设有一个动态生成的元素,给它添加了事件监听器,但没有移除。
        function createAndAddEvent() {
            const div = document.createElement('div');
            div.id = 'myDiv';
            document.body.appendChild(div);
            div.addEventListener('click', function() {
                console.log('Div clicked');
                // 做一些操作
            });
            // 一段时间后移除元素
            setTimeout(function() {
                document.body.removeChild(div);
                // 这里没有移除事件监听器
            }, 3000);
        }
        createAndAddEvent();
        
        • 在这个例子中,元素 div 被创建并添加了点击事件监听器。然后在一段时间后,元素被移除了,但由于没有移除事件监听器,事件监听器函数仍然会占用内存,导致内存泄漏。正确的做法应该是在移除元素之前移除事件监听器,即在 setTimeout 的回调函数中添加 div.removeEventListener('click', function() {...}),但由于匿名函数的原因,这在实际操作中比较复杂。所以在实际开发中,应该尽量使用命名函数来添加和移除事件监听器,以便能够正确地移除它们。
    • 被忽略的全局变量

      • 背景 :在 JavaScript 中,如果不小心声明了全局变量(例如,忘记使用 varletconst 声明变量),那么这个变量就会成为全局对象的属性。全局对象的生命周期和程序一样长,所以这些变量会一直占用内存,直到程序结束。

      • 举例

        • 假设在函数中不小心创建了全局变量。
        function createGlobalLeak() {
            // 忘记使用 var、let 或 const 声明变量
            myGlobalVar = new Array(1000000).join('*'); // 创建一个比较大的数据
        }
        createGlobalLeak();
        // 此时,myGlobalVar 成为全局对象的属性,在程序结束前都不会被回收,造成内存泄漏
        
        • 在这个例子中,myGlobalVar 被隐式地声明为全局变量,它会一直占用内存,直到程序结束,从而可能导致内存泄漏。