闭包的内存泄露与优化
JavaScript闭包导致的内存泄露原理、检测方法与优化策略。
1. 闭包与内存关系
1.1 闭包的内存持有
闭包会持有外部函数的变量引用,阻止垃圾回收:
function createHandler() {
const hugeData = new Array(1000000).fill('x'); // ~8MB
return function handler() {
console.log(hugeData.length); // 只用了 length,但整个数组无法回收
};
}
const handler = createHandler();
// hugeData 被 handler 闭包引用,无法被 GC 回收
1.2 闭包作用域链
全局作用域
└── createHandler 作用域
├── hugeData (Array[1000000]) ← 被 handler 闭包持有
└── handler 函数
└── [[Scope]] → createHandler 作用域
2. 常见泄露模式
2.1 意外的闭包捕获
// 问题:闭包捕获了不需要的变量
function setup() {
const element = document.getElementById('btn');
const largeConfig = {/* 大量配置数据 */};
element.addEventListener('click', function () {
// 只用了 element,但 largeConfig 也被闭包持有
console.log('clicked', element.id);
});
}
// 优化:只捕获需要的值
function setup() {
const element = document.getElementById('btn');
const elementId = element.id; // 提取需要的值
element.addEventListener('click', function () {
console.log('clicked', elementId); // 闭包只持有 elementId
});
// largeConfig 可被 GC 回收
}
2.2 定时器中的闭包
// 问题:定时器持有闭包引用
function startPolling() {
const data = fetchHugeData();
setInterval(function () {
process(data); // data 被闭包持有,永远不会被回收
}, 1000);
}
// 优化:及时清除定时器
function startPolling() {
const data = fetchHugeData();
const timerId = setInterval(function () {
process(data);
}, 1000);
// 提供停止方法
return function stop() {
clearInterval(timerId);
// timerId 被清除后,闭包可被 GC 回收
};
}
const stop = startPolling();
// 不需要时调用
stop();
2.3 循环中的闭包
// 问题:所有闭包共享同一个变量引用
function createCallbacks() {
const callbacks = [];
for (var i = 0; i < 1000; i++) {
callbacks.push(function () {
console.log(i); // 所有回调都引用同一个 i = 1000
});
}
return callbacks;
}
// 优化1:使用 let
function createCallbacks() {
const callbacks = [];
for (let i = 0; i < 1000; i++) {
callbacks.push(function () {
console.log(i); // 每个回调有自己的 i
});
}
return callbacks;
}
// 优化2:IIFE 隔离作用域
function createCallbacks() {
const callbacks = [];
for (var i = 0; i < 1000; i++) {
callbacks.push(
(function (j) {
return function () {
console.log(j);
};
})(i)
);
}
return callbacks;
}
2.4 DOM 引用泄露
// 问题:闭包持有 DOM 引用,DOM 删除后仍无法回收
function createCache() {
const cache = {};
return {
set(key, element) {
cache[key] = element; // 持有 DOM 元素引用
},
get(key) {
return cache[key];
},
};
}
const cache = createCache();
const btn = document.getElementById('btn');
cache.set('btn', btn);
// 即使从 DOM 中移除按钮
document.body.removeChild(btn);
// cache 仍持有 btn 引用 → DOM 节点无法被 GC 回收
// 优化:使用 WeakRef 或 WeakMap
function createCache() {
const cache = new WeakMap();
return {
set(key, element) {
cache.set(key, element);
},
get(key) {
return cache.get(key);
},
};
}
3. 检测方法
3.1 Chrome DevTools Memory 面板
1. 打开 DevTools → Memory
2. 选择 "Heap snapshot"
3. 操作页面(触发闭包创建)
4. 再次拍摄快照
5. 对比两个快照,查找 Detached DOM 节点或持续增长的对象
3.2 Performance Monitor
1. 打开 DevTools → 更多工具 → Performance Monitor
2. 观察 JS heap size 是否持续增长
3. 手动触发 GC(点击垃圾桶图标)
4. 如果 GC 后内存未恢复,说明存在泄露
3.3 编程检测
// 简单的内存增长检测
if (typeof process !== 'undefined') {
const before = process.memoryUsage().heapUsed;
// 执行操作
for (let i = 0; i < 10000; i++) {
createHandler();
}
global.gc?.(); // 需要 --expose-gc 标志
const after = process.memoryUsage().heapUsed;
console.log(`Memory delta: ${(after - before) / 1024 / 1024} MB`);
}
4. 优化策略
4.1 及时解除引用
function process() {
let largeData = loadData();
const result = compute(largeData);
// 处理完后解除引用
largeData = null;
return result;
}
4.2 使用 WeakMap / WeakRef
// WeakMap: 键是弱引用,键对象可被 GC 回收
const metadata = new WeakMap();
function attachMetadata(element, data) {
metadata.set(element, data);
// 当 element 从 DOM 移除并被 GC 回收时,metadata 中的条目自动清除
}
4.3 使用 FinalizationRegistry
const registry = new FinalizationRegistry((heldValue) => {
console.log(`对象被回收: ${heldValue}`);
// 执行清理操作
});
function createTrackedObject() {
const obj = { data: new Array(1000000) };
registry.register(obj, 'tracked-object');
return obj;
}
4.4 闭包最小化原则
// 闭包捕获了整个 config
function createProcessor(config) {
return function (data) {
return data.map((item) => config.transform(item));
};
}
// 闭包只捕获需要的函数
function createProcessor(config) {
const { transform } = config; // 解构提取
return function (data) {
return data.map((item) => transform(item));
};
// config 的其他属性可被 GC 回收
}