- 什么是闭包?
- 闭包导致内存泄漏的场景?
- 为什么循环引用会导致内存泄露?
- 如何检测循环引用?
- 如何避免循环引用导致的内存泄露?
什么是闭包
闭包是指函数在创建时,保存了对其定义作用域的引用。也就是在执行函数时,函数内部可以访问其词法作用域外的变量。在编程领域中,也可以简单的理解为闭包就是绑定了执行环境的函数。 在 JS 中,闭包通常以函数嵌套函数的形式出现。
例如:
function outerFn(){
let num = 1;
return innerFn(){
num++
console.log(num)
}
}
const fn = outerFn()
fn()
在上述代码中,innerFn就是一个闭包函数,即使在 outerFn 函数执行完后,还能访问 num 变量。在 JS 中,函数是“第一类公民”,可以作为返回值返回或传递,因此在外部函数返回后,闭包依然保留对外部变量的访问权限。
闭包导致内存泄漏的场景?
在 JS 中闭包有时会导致内存泄露。因为闭包在访问外部作用域的变量时,会让这些变量没法被垃圾回收,从而导致内存泄露。常见的场景是:① 未清理的事件监听 ② 未清理的定时器
- 未清理的事件监听
function addEvent(){
const el = document.getElementById("button");
const someData = "demo data";
el.addEventListener("click", ()=>{
console.log(someData)
})
}
addEvent();
在上述代码中,button 按钮的点击监听事件的回调函数引用了外部变量 someData,如果在不需要时没有移除监听器,那么闭包一直存在,someData永远无法被释放,造成内存泄露。
- 未清理的定时器
function createTimer(){
let count = 0;
setInterval(function(){
count++;
console.log(count); // 定时器闭包包含了 count 的引用
})
}
createTimer();
在上述代码中,定时器的回调函数中使用了闭包,但在不需要时没有清除定时器,导致回调函数及其引用的外部变量无法被回收。
为什么循环引用会导致内存泄露?
什么是循环引用?
循环引用是指两个或多个对象相互引用,从而形成了一个循环结构,导致垃圾回收器无法回收这些对象。
为什么循环引用会导致内存泄露?
JS 的垃圾回收机制采用了“标记-清除”算法,垃圾回收器会从根对象(如全局对象出发)查找所有可达对象。若对象形成了循环引用,且不再被根对象访问,则垃圾回收器无法将其清除,这导致这些对象长期保留在内存中,形成内存泄露。
举个简单的例子:
let objA = {};
let objB = {};
objA.ref = objB;
objB.ref = objA;
上述代码中,objA 引用了 objB,objB又引用了 objA 造成了循环引用。如果没有外部引用它们,按理是可以被垃圾回收,但由于相互持有的引用,导致它们无法被清除,形成了内存泄露。
如何检测循环引用?
有三种方式:手动检测、JSON.stringfy()检测、浏览器的开发者工具检测
- 手动检测
在代码中通过逻辑分析或者console.log输出检查对象的相互引用关系
- JSON.stringfy()检测
JSON.stringfy检测,只能检测较简单的循环引用情况,较复杂的情况需要结合手动检测或者开发者工具进行。 通过 JSON.stringfy 序列化对象,如果对象中存在循环引用则会抛出 TypeError 异常。
例如下列代码,通过拦截错误判断是否存在循环引用:
function isCircularReference(obj){
try{
JSON.stringfy(obj);
return false; // 无循环引用
}
catch(error){
return true; // 有循环引用
}
}
let objA = {};
let objB = { ref: objA };
objA.ref = objB;
console.log(isCircularReference(objA)); // true
- 浏览器的开发者工具检测
现代浏览器的开发者工具提供了 内存快照 和 堆分析,可以捕获内存快照来分析内存的使用情况,帮助发现循环引用和内存泄露。例如谷歌的开发者工具中,通过 内存 面板,使用 堆快照 来查看对象的引用关系,并检查是否有意外的循环引用。
如何避免循环引用导致的内存泄露?
有三类措施:人为控制避免对象互相引用、使用 WeakMap/WeakSet 弱引用结构存储对象间的引用关系、在不需要时手动断开引用,设置引用为 null 或 undefined。
- 避免对象互相引用
在设计数据结构时,尽量避免相互引用,尤其是大的复杂对象。
- 使用 WeakMap/WeakSet
在 JS 中, WeakMap 和 WeakSet 是弱引用结构,存在 WeakMap 和 WeakSet 中的对象不会阻止垃圾回收。可以使用 WeakMap 和 WeakSet 来存储对象之间的引用关系,从而避免循环引用导致的内存泄漏。
const wM = new WeakMap();
const objA = {};
const objB = {};
wM.set(objA, objB);
- 在不需要时手动断开引用
当对象不再使用时,手动将引用设为 null 或 undefined,确保垃圾回收器能正常回收它们。
let objA = {};
let objB = {};
objA.ref = objB;
objB.ref = objA;
// 不再用的时候,断开引用关系
objA.ref = null;
objB.ref = undefined;