大家好,我是第八哥,做互联网开发 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 = { scrollTop: 0 };
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面板记录操作,看内存是否持续上涨,上涨的话大概率有泄漏。
总结一下
闭包不是洪水猛兽,只要记住:不用的引用及时清,定时器、事件监听器别忘删。
多写多测,遇到内存问题不慌,按上面的方法排查,准能搞定。
评论