Hello,大家好,我是 Sunday。
在最近的中小厂面试中,【闭包】的问题被很多公司提到。如果单纯说闭包是比较简单的,一句话就可以说清楚:“可以访问其他函数作用域中变量的函数,就是闭包函数”。
但是,随后延伸的问题,如:闭包造成内存泄漏的场景、循环引用为什么导致内存泄露?怎么判断是否存在循环引用? 等问题,很多同学回答的并不好。
因此,这篇文章就跟大家详细的说一下关于闭包的问题,争取可以做到让大家看完这篇文章之后,对比闭包的问题可以顺畅回答!
1. 什么是闭包
闭包是指 函数在创建时保留了对其定义作用域的引用,即使函数执行在其词法作用域之外,也能访问该作用域中的变量。
闭包在 JavaScript 中的常见表现形式是:函数嵌套函数,内部函数访问外部函数的变量。
由于 JavaScript 的函数是“第一类公民”,可以作为值返回、传递或保存,因此在外部函数返回后,闭包依然保留对外部变量的访问权限。
function outerFunction() {
let counter = 0;
return function innerFunction() {
counter++;
console.log(counter);
};
}
const increment = outerFunction();
increment(); // 输出: 1
increment(); // 输出: 2
在上述代码中,innerFunction 是一个闭包,它可以访问 outerFunction 中的变量 counter,即使 outerFunction 已经执行完毕。
2. 闭包导致的内存泄露场景
在 JS 中,闭包有时会导致内存泄露,这是因为:闭包在访问外部作用域的变量时会让这些变量无法被垃圾回收,从而导致不必要的内存占用。
2.1. 常见的内存泄露场景
- 未清理的事件监听:如果事件监听器引用了外部作用域中的变量,且在不需要时未移除,则会导致闭包一直存在,无法释放内存。
function addEvent() {
const element = document.getElementById('button');
const someData = "Important data";
element.addEventListener('click', function() {
console.log(someData); // 闭包引用了外部变量 someData
});
}
addEvent();
// 这里如果不手动移除事件监听器,则 someData 永远不会被释放,造成内存泄露
- 定时器未清理:在定时器的回调函数中使用了闭包,但在不再需要时未清除定时器,导致回调函数及其引用的外部变量无法被回收。
function createTimer() {
const largeData = new Array(10000).fill('*');
setInterval(function() {
console.log(largeData); // 定时器闭包持有 largeData 的引用
}, 1000);
}
createTimer();
// 这里如果不清除定时器,largeData 将永远无法释放
3. 循环引用导致内存泄露
循环引用是指:两个或多个对象相互引用,从而形成一个循环结构,导致垃圾回收器无法回收这些对象。
3.1. 为什么循环引用会导致内存泄露?
JS 的垃圾回收机制使用 标记清除(mark-and-sweep) 算法。即:垃圾回收器会从根对象(如全局对象)出发,查找所有可达对象。
若对象形成了循环引用,且不再被根对象访问,则垃圾回收器无法将其清除,这会导致这些对象长期保留在内存中,形成内存泄露。
function createCircularReference() {
const objectA = {};
const objectB = {};
objectA.ref = objectB; // objectA 引用 objectB
objectB.ref = objectA; // objectB 引用 objectA,形成循环引用
}
createCircularReference();
// 这里 objectA 和 objectB 都无法被回收
在这个示例中,objectA 和 objectB 互相引用,形成了循环引用。如果没有外部引用它们,按理说可以被垃圾回收,但由于相互持有的引用,导致它们无法被清除,形成内存泄露。
4. 如何检测循环引用
在项目中,如果出现 内存泄漏 的问题,那么可以通过以下方式进行检查:
- 手动检测:在代码中通过逻辑分析或使用 console.log 输出检查对象的相互引用关系。
- 使用开发者工具检测:现代浏览器的开发者工具提供了内存快照和堆分析,可以捕获内存快照来分析内存的使用情况,帮助发现循环引用和内存泄露。在 Chrome 开发者工具中,可以通过 Memory(内存) 面板,使用 Heap Snapshot(堆快照)来查看对象的引用关系,并检查是否有意外的循环引用。
图片
- JSON.stringify 检测:尝试使用 JSON.stringify 序列化对象,如果对象中存在循环引用,JSON.stringify 会抛出 TypeError 异常,可以用这种方式简单检测循环引用(注意这种方法只能用于检测较简单的循环引用,复杂场景需结合其他方法)。
function hasCircularReference(obj) {
try {
JSON.stringify(obj);
return false; // 无循环引用
} catch (error) {
return true; // 有循环引用
}
}
const objectA = {};
const objectB = { ref: objectA };
objectA.ref = objectB;
console.log(hasCircularReference(objectA)); // 输出: true
- WeakMap 弱引用:使用 WeakMap 结构管理对象引用。由于 WeakMap 的键是弱引用,不会影响对象的垃圾回收,可以通过 WeakMap 追踪对象引用关系,并避免循环引用导致的内存泄露。
5. 如何避免循环引用导致的内存泄露
如果检测出现内存泄漏的问题,那么可以通过以下方式尝试解决:
- 避免对象互相引用:在设计数据结构时,尽量避免互相引用,尤其是大的复杂对象。
- 使用 WeakMap 或 WeakSet:在 JavaScript 中,WeakMap 和 WeakSet 是弱引用结构,存储在 WeakMap 或 WeakSet 中的对象不会被阻止垃圾回收。可以使用 WeakMap 和 WeakSet 来存储对象之间的引用关系,避免循环引用导致的内存泄露。
const weakMap = new WeakMap();
const objectA = {};
const objectB = {};
weakMap.set(objectA, objectB);
- 在不需要时手动断开引用:当对象不再使用时,可以手动将引用设为 null 或 undefined,确保垃圾回收器能够正常回收它们。
let objectA = {};
let objectB = {};
objectA.ref = objectB;
objectB.ref = objectA;
// 当不再需要时,断开引用关系
objectA.ref = null;
objectB.ref = null;