前置知识: JavaScript

闭包的内存泄露与优化

1 minAdvanced2026/6/14

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 回收
}