一、什么是内存泄漏?

内存泄漏就像是你租了一间房子,合同到期后却忘记退租,房东也没发现,结果你一直白白交着房租。在JavaScript中,内存泄漏指的是程序不再需要的内存没有被垃圾回收机制(GC)释放,导致可用内存越来越少,最终可能引发页面卡顿甚至崩溃。

举个简单的例子:

// 技术栈:JavaScript(浏览器环境)
function createLeak() {
  const hugeArray = new Array(1000000).fill('leak'); // 创建一个超大数组
  document.getElementById('leakBtn').addEventListener('click', () => {
    console.log(hugeArray.length); // 闭包引用了hugeArray,导致无法释放
  });
}
createLeak();
// 即使createLeak执行完毕,hugeArray仍被事件监听器引用,无法被GC回收

这里,hugeArray被事件监听器的回调函数引用,即使createLeak函数执行完毕,内存也无法释放。

二、常见的内存泄漏模式

1. 意外的全局变量

JavaScript中,如果忘记声明变量(比如写错letconst),变量会变成全局的,直到页面关闭才会释放。

// 技术栈:JavaScript
function leakGlobal() {
  leak = 'I am a global variable!'; // 本意是局部变量,但漏了var/let/const
}
leakGlobal();
// 此时window.leak存在,直到页面关闭

解决方法:严格模式('use strict')会直接报错,避免这种问题。

2. 被遗忘的定时器和回调

定时器(setInterval/setTimeout)和事件监听器如果不清除,会一直持有引用。

// 技术栈:Node.js(setInterval示例)
const { EventEmitter } = require('events');
const emitter = new EventEmitter();

function startLeakyTimer() {
  setInterval(() => {
    emitter.emit('ping'); // 定时器持续运行,即使不再需要
  }, 1000);
}
startLeakyTimer();

// 正确的做法是保存timerId,并在适当时clearInterval

3. DOM引用未清理

手动保存的DOM元素引用,即使节点从页面移除,只要引用存在,内存就不会释放。

// 技术栈:浏览器JavaScript
const elements = {
  button: document.getElementById('oldButton'), // 保存DOM引用
};

document.body.removeChild(document.getElementById('oldButton')); // 从DOM移除
// 但elements.button仍引用该节点,内存无法回收

4. 闭包滥用

闭包是JavaScript的强大特性,但过度使用会导致变量长期驻留内存。

// 技术栈:JavaScript
function createClosureLeak() {
  const data = 'Sensitive Data'; // 本应短期存在的变量
  return function() {
    console.log(data); // 闭包导致data无法释放
  };
}
const leakedFn = createClosureLeak();
// 即使不再需要leakedFn,data仍被持有

三、排查内存泄漏的技巧

1. 使用开发者工具

Chrome DevTools的Memory面板是神器:

  1. 拍下堆快照(Heap Snapshot)。
  2. 执行可疑操作。
  3. 再拍快照,对比前后变化,找到未被释放的对象。

2. 监控内存变化

通过performance.memory(浏览器)或process.memoryUsage()(Node.js)实时观察:

// 技术栈:Node.js
setInterval(() => {
  const memory = process.memoryUsage();
  console.log(`RSS: ${memory.rss / 1024 / 1024} MB`); // 常驻内存
}, 1000);

3. 代码审查

重点关注:

  • 全局变量。
  • 未清理的定时器/事件监听器。
  • 大型数据结构(如缓存未设置上限)。

四、实战:修复一个真实案例

假设我们有一个单页应用(SPA),用户反馈切换页面后越来越卡。

问题代码

// 技术栈:Vue.js(但问题通用)
export default {
  mounted() {
    window.addEventListener('resize', this.handleResize); // 添加监听
  },
  methods: {
    handleResize() {
      // 处理逻辑
    },
  },
  // 忘记在beforeDestroy中移除监听!
};

修复方案

export default {
  mounted() {
    window.addEventListener('resize', this.handleResize);
  },
  beforeDestroy() {
    window.removeEventListener('resize', this.handleResize); // 清理监听
  },
  methods: {
    handleResize() {
      // 处理逻辑
    },
  },
};

五、总结

内存泄漏的核心原因是“该释放的没释放”。预防的关键在于:

  1. 避免隐式全局变量。
  2. 及时清理定时器、事件监听器和DOM引用。
  3. 合理使用闭包。
  4. 善用开发者工具排查。

在大型应用中,内存问题可能不会立刻暴露,但随着时间推移,小漏洞会累积成大问题。养成良好的编码习惯,定期进行内存检查,才能让应用长期稳定运行。