一、内存的生命周期:从分配到释放

所有编程语言的运行都离不开内存的分配与释放。内存的生命周期通常包括三个步骤:

  • 申请内存:为变量、对象等分配一块内存空间;
  • 使用内存:读写内存中的数据;
  • 释放内存:不再需要时及时释放,防止资源浪费。

不同语言有不同的内存管理方式:

  • 手动管理:如 C/C++ 需要开发者手动调用 malloc 和 free;
  • 自动管理:如 Java、JavaScript、Python 等,通过垃圾回收机制(GC)自动释放内存。

JavaScript 属于自动内存管理语言,开发者通常无需手动释放内存,但理解其原理依然至关重要。

二、JavaScript 内存的分配方式

在 JS 中,变量的内存分配依赖于数据类型:

  • 基本数据类型(number, string, boolean, null, undefined, symbol, bigint):分配在栈空间,存储的是实际值。
  • 引用数据类型(object, array, function):分配在堆内存中,变量保存的是指向堆中实际数据的引用(地址)。

这种区分不仅影响内存管理,也影响变量复制和比较等行为。

三、垃圾回收机制:谁来释放内存?

由于内存资源有限,JavaScript 引擎必须具备一套自动释放机制 —— 垃圾回收机制(GC, Garbage Collection)。GC 会周期性地检查内存中哪些对象不再被引用,并将其销毁释放。

常见的垃圾回收算法

1. 引用计数(Reference Counting)

  • 每个对象记录被引用的次数;
  • 引用数为 0 时即表示对象“无主”,可被销毁;
  • 缺点:容易因循环引用导致内存泄漏。

2. 标记清除(Mark and Sweep)

  • 从根对象出发,递归标记所有可以访问到的对象;
  • 未被标记的即为“垃圾”对象,将被清除;
  • 这是 V8 引擎等主流 JavaScript 引擎采用的主要算法。

标记清除有效解决了引用计数的问题,因此被广泛采用。

四、闭包:最迷人的“陷阱”

什么是闭包?

闭包(Closure)是 JS 中的一个核心概念,也是许多开发者难以掌握的知识点。

计算机科学定义

闭包是函数与其词法作用域(即定义时的变量环境)的绑定。当函数脱离原作用域后,依然可以访问原有的变量。

MDN 定义

闭包是指“一个函数和其相关的作用域变量捆绑在一起”的组合体。

示例代码

function makeAdder(x) {
  return function(y) {
    return x + y;
  };
}

const add10 = makeAdder(10);
console.log(add10(5)); // 输出 15在这个例子中:
  • makeAdder 返回的匿名函数访问了外部的 x;
  • 即使 makeAdder 执行完毕,其作用域环境(包含 x 的值)依然被保留;
  • 这就是闭包在起作用。

五、闭包的内存问题:内存泄漏的根源?

闭包虽然强大,但如果使用不当,也可能带来内存泄漏的问题。

为什么会内存泄漏?

当闭包保持对外部作用域变量的引用时,即使外部函数已经执行完毕,其作用域中的变量(如 AO 对象)仍然不会被 GC 回收。

如下图示意(简化):

add10 → 0xb00 函数对象 → AO(0x200)包含变量 x如果 add10 一直存在于全局作用域;

  • 那么 0xb00 这个函数对象就无法销毁;
  • 进而其引用的 AO 对象也无法释放。

这就是闭包可能引发的 内存泄漏链。

如何避免?

只需在不再使用闭包时,手动断开引用即可:

add10 = null;这样,GC 在下一轮检查中会发现这些对象不可达,进而释放内存。

六、闭包的变量释放:全都留着吗?

一个常见问题是:闭包环境中的所有变量都会被保留吗?

答案是否定的。JS 引擎会在某些情况下优化,只保留闭包中实际使用到的变量。

示例:

function test() {
  let name = "why";
  let count = 0;

  return function() {
    count++;
    console.log(count);
  };
}在这个例子中,虽然 name 在作用域中,但闭包中并未引用它,因此引擎可能会将其销毁以节省内存。

总结

JavaScript 的内存管理机制与闭包机制密不可分。

  • JS 如何分配和释放内存;
  • 垃圾回收机制的核心原理;
  • 闭包的定义、使用场景与潜在问题;
  • 如何优雅避免闭包导致的内存泄漏。