JavaScript闭包陷阱:10个内存泄漏场景+修复指南 - 资深开发实战技巧

大家好,我是第八哥,做互联网开发 10 年了,闭包这东西,真是让人又爱又恨。

它能帮我们封装变量、延长作用域,可是稍不注意,它就会变成内存泄漏的“隐形杀手”。今天就来聊聊闭包的那些坑,还有怎么填。

什么是闭包导致的内存泄漏?

简单地说,闭包会保留对外部变量的引用。如果这些变量是 DOM、定时器之类的“大家伙”,且又没有得到及时释放,那么内存就会越堆越多。从而导致用户体验变卡,严重时还会崩溃。

咱们做开发的,可不能让这事儿发生啊。

1. 意外保留 DOM 元素的闭包

我之前遇过这么个情况:写了个列表项点击事件,然后用闭包存储了DOM节点,后来节点被移除了,可闭包还拿着引用没放手。

function createItem() {
    const item = document.createElement('li');
    item.onclick = () => {
        console.log(item.innerHTML); // 闭包引用 item
    };
    return item;
}

我的修复方法是:不用时手动解除引用,比如在节点移除前设置 item.onclick = null

2. 定时器里的闭包陷阱

定时器加闭包,简直是“黄金搭档”级别的坑。setInterval里的闭包引用了大对象,定时器不清除,对象就一直占内存。

let data = { big'...' }; // 大对象
setInterval(() => {
    console.log(data.big); // 闭包引用data
}, 1000);

定时器不用时一定要记得用clearInterval及时清除,同时把data设为null。

3. 事件监听器未移除的闭包

当需要监听页面滚动来实现一些动态效果时,我们会给window对象添加scroll监听,同时用闭包存储滚动状态。当组件销毁时,如果没及时解绑,闭包就会一直“抓着”状态不放。

function init() {
    const status = { scrollTop0 };
    window.addEventListener('scroll'() => {
        status.scrollTop = window.scrollY// 闭包引用status
    });
}

在离开页面或组件销毁时,用 removeEventListener 解绑就好了。

4. 缓存过度的闭包

用闭包做缓存挺方便的,但缓存太多了且不清理,就成了内存负担。我见过有人缓存了上百条数据,结果页面卡得动不了。

function createCache() {
    const cache = {};
    return {
        set(key, val) => { cache[key] = val; },
        get(key) => cache[key]
    };
}
const cache = createCache();

在使用缓存时加个过期清理机制,比如定时删除旧数据,或者限制缓存大小,避免过多的内存占用导致页面卡死,从而影响用户体验,那就有点得不偿失了。

5. 闭包中的循环引用

当两个对象互相引用,且又被闭包包裹时,GC(垃圾回收)就很难识别,那么内存自然也就漏了。

function loopRef() {
    const obj1 = {};
    const obj2 = {};
    obj1.ref = obj2;
    obj2.ref = obj1;
    return () => {
        console.log(obj1, obj2);
    };
}
const fn = loopRef();

当对象不再使用时,一定要手动切断引用,比如 obj1.ref = null; obj2.ref = null;

6. 全局变量被闭包引用

全局变量本身就难回收,要是再被闭包引用,更是“雪上加霜”。我见过有人在闭包里改全局数组,结果数组越变越大。

let globalArr = [];
function addData() {
    return (data) => {
        globalArr.push(data);
    }; // 闭包引用全局变量
}
const add = addData();

尽量别让闭包碰全局变量,如果非用不可的话,一定要及时清空globalArr。

7. 类实例中的闭包泄漏

在类的方法里用闭包引用了this,当实例销毁后,闭包还拿着this,实例就回收不了了。

class MyClass {
    constructor() {
        this.data = '...';
        this.handler = () => {
            console.log(this.data);
        }; // 闭包引用this
    }
}
let instance = new MyClass();
instance = null// 销毁实例,但handler还在引用this

我们在类的destroy方法里,把handler设为null,从而切断引用。

8. 闭包嵌套过深

多层闭包嵌套,每层都引用变量,就像串起一串“内存包袱”,GC很难理清。

function outer() {
    const a = 'a';
    function middle() {
        const b = 'b';
        function inner() {
            console.log(a, b);
        } // 引用外层变量
        return inner;
    }
    return middle();
}
const fn = outer();

实际开发中,要减少嵌套的层数,把不需要的变量及时设为null。

9. 闭包中的DOM事件委托滥用

事件委托虽好,但当闭包在委托里引用了大量的DOM时,父节点没移除,子节点的引用就一直留着。

const parent = document.getElementById('parent');
parent.addEventListener('click'(e) => {
    const child = e.target;
    console.log(child.dataset.id); // 闭包引用child
});

当不再使用时要及时解绑事件,或者避免在闭包里存DOM引用。

10. 闭包与第三方库的冲突

在开发中,我们经常会用到第三方库来实现一些功能或效果,来简化开发。在使用第三方库时,如果闭包不小心引用了库的实例,且库本身没清理,咱们的闭包又抱着不放,那么内存就漏了。

import SomeLib from 'some-lib';
function useLib() {
    const libInstance = new SomeLib();
    return () => {
        libInstance.doSomething();
    }; // 闭包引用实例
}
const fn = useLib();

我们要及时调用库的销毁方法,再把libInstance设为null。

闭包内存泄漏的排查技巧

在问题排查时,Chrome的Memory面板很好用,我们先录制初始堆快照,然后执行疑似泄漏操作后生成第二个快照,对比两次快照,看看哪些对象没被回收。

也可以用performance面板记录操作,看内存是否持续上涨,上涨的话大概率有泄漏。

总结一下

闭包不是洪水猛兽,只要记住:不用的引用及时清,定时器、事件监听器别忘删。

多写多测,遇到内存问题不慌,按上面的方法排查,准能搞定。

上一篇 CSS常见问题解答:前端初级开发者必学实战技巧、优缺点及示例代码解析 下一篇 Web Workers多线程调试实战指南:解决内存隔离与通信异常的5大技巧 | 前端性能优化

评论

暂不支持评论