阅读视图

发现新文章,点击刷新页面。

JavaScript 内存机制与闭包:从栈到堆的深入浅出

在前端开发中,JavaScript 是我们最常用的编程语言之一。然而,很多人在学习 JS 的过程中,常常忽略了它背后运行的底层机制——内存管理。今天我们就结合几段代码和图示,来聊聊 JavaScript 中最重要的两个概念:内存机制闭包


一、JS 的执行环境:三大内存空间

在 JavaScript 执行过程中,程序会使用三种主要的内存空间:

  1. 代码空间
  2. 栈内存(Stack)
  3. 堆内存(Heap)

lQLPJxDjlfllrWfNBJ_NBHawALUXmftxe5cJEmLLH90MAA_1142_1183.png

图1:JavaScript 的三大内存空间

1. 代码空间

这是存放源代码的地方。当浏览器加载 HTML 文件时,会把 <script> 标签中的代码从硬盘读取到内存中,形成“代码空间”。这部分内容不会直接参与运行,而是供引擎解析和编译用。

2. 栈内存

栈内存是 JS 执行的主角,用于维护函数调用过程中的执行上下文。它的特点是:

  • 空间小、速度快
  • 连续存储,便于快速切换
  • 每次函数调用都会创建一个执行上下文并压入栈顶
function foo() {
  var a = 1;
  var b = a;
  a = 2;
  console.log(a); // 2
  console.log(b); // 1
}
foo();

这段代码执行时,foo() 被调用,会生成一个新的执行上下文,并压入调用栈。这个上下文包含变量 ab,它们都是简单数据类型,直接存储在栈中。

3. 堆内存

堆内存用来存储复杂数据类型,比如对象、数组等。这些数据体积大、结构复杂,并且是动态的,不能放在栈里,所以被分配到堆中。 它的特点是:

  • '辅助栈内存'
  • 空间大 不连续
  • 存储复杂数据类型 对象
function foo() {
  var a = { name: '极客时间' };
  var b = a; // 引用拷贝
  a.name = '极客邦';
  console.log(a); // {name: "极客邦"}
  console.log(b); // {name: "极客邦"}
}
foo();

这里 a 是一个对象,它实际存储在堆内存中,而 a 变量只是保存了该对象的地址(引用)。b = a 并不是复制对象,而是让 b 也指向同一个地址。因此修改 a.name 后,b 也能看到变化。

为什么堆内存是不连续的?

  • 因为对象是动态的 可以去给它添加属性或方法 如果是连续的情况下 显然就不好进行操作了

二、简单 vs 复杂 数据类型:存储方式不同

JavaScript 有八种原始数据类型:

undefined, null, boolean, string, number, symbol, bigint, object

其中前七种是简单数据类型,最后一个是复杂数据类型

类型 存储位置
简单类型(如 number, string) 栈内存
复杂类型(如 object, array) 堆内存

示例说明

var a = '极客时间'; // 字符串 → 栈内存
var b = a;         // 拷贝值 → b 也是 '极客时间'
a = '极客邦';      // 修改 a 不影响 b
console.log(a); // 极客邦
console.log(b); // 极客时间
var c = { name: '极客时间' }; // 对象 → 堆内存
var d = c;                  // 引用拷贝 → d 指向同一地址
c.name = '极客邦';          // 修改共享对象
console.log(c.name); // 极客邦
console.log(d.name); // 极客邦

✅ 结论:

  • 简单类型是“值传递”,每个变量独立存在
  • 复杂类型是“引用传递”,多个变量可能指向同一块堆内存

lQLPJxNqPKGZwofNAifNBHawc4pej93FtxkJEmmk8M9qAA_1142_551.png

图2:变量 c 存储的是堆内存中对象的地址(1003)


三、执行上下文与调用栈

JavaScript 的执行流程依赖于调用栈(Call Stack),它是函数调用的“记录本”。

每当一个函数被执行,就会创建一个执行上下文(Execution Context),包括:

  • 变量环境(Variable Environment)
  • 词法环境(Lexical Environment)
  • outer(外层作用域链)

执行上下文会被压入调用栈顶部。当函数执行完毕后,该上下文就会被弹出并回收。

示例:函数调用前后

function foo() {
  var a = 1;
  var b = 2;
}

foo(); // 调用 foo

执行过程如下:

  1. 创建全局执行上下文(已存在)
  2. 调用 foo(),创建新的执行上下文,压入调用栈
  3. 执行完 foo(),将其执行上下文弹出,指针回到全局上下文

lQLPJw1WF0yif-fNAhTNBHawwsioxubOAzAJEmTtM6BTAA_1142_532.png

图3:foo() 执行前后调用栈的变化

注意:栈顶指针移动非常快,因为栈是连续内存,只需要改变指针位置即可完成上下文切换(栈顶指针的切换通过一个机制,做内存的减法)。如果将复杂对象也放在栈中,会导致栈空间过大、不连续,严重影响性能。


四、动态弱类型语言:JS 的灵活性

JavaScript 是一种动态弱类型语言,这意味着:

  • 动态:变量可以在运行时改变类型
  • 弱类型:不同类型之间可以自动转换
var bar;
console.log(typeof bar); // undefined

bar = 12;
console.log(typeof bar); // number

bar = '极客时间';
console.log(typeof bar); // string

bar = true;
console.log(typeof bar); // boolean

bar = null;
console.log(typeof bar); // object(JS 设计缺陷)

bar = { name: '极客时间' };
console.log(typeof bar); // object

这正是 JS 的魅力所在:无需提前声明类型,灵活自由。但这也带来了潜在问题,比如 typeof null === 'object' 就是一个经典 bug。


五、闭包的本质:延长变量生命周期

现在我们来看一个关键概念——闭包

什么是闭包?

闭包是指:内部函数能够访问外部函数的变量,即使外部函数已经执行完毕。

<script>
function foo() {
  var myName = "极客时间";
  let test1 = 1;
  const test2 = 2;

  var innerBar = {
    setName: function (newName) {
      myName = newName;
    },
    getName: function () {
      console.log(test1);
      return myName;
    }
  };

  return innerBar;
}

var bar = foo();
bar.setName("极客邦");
bar.getName(); // 输出:1, 极客邦
</script>

在这个例子中,innerBar 返回了一个对象,其方法 setNamegetName 都能访问 myNametest1。但 foo() 已经执行完了,按理说这些变量应该被销毁了,为什么还能访问?

这就是闭包的作用!


六、闭包背后的内存机制

闭包是如何工作的?答案就在堆内存中。

步骤解析:

  1. foo() 被调用时,V8 引擎会扫描内部函数 setNamegetName
  2. 发现这两个函数引用了外部变量 myNametest1
  3. 引擎判断:这是一个闭包!需要保留这些变量
  4. 堆内存中创建一个特殊的对象:closure(foo)
  5. myNametest1 的值保存到这个对象中
  6. closure(foo) 的地址存入栈中(作为 innerBar 的一部分)

lQLPJxNqPKGZwofNAifNBHawc4pej93FtxkJEmmk8M9qAA_1142_551.png

图4:闭包 closure(foo) 存储在堆内存中,栈中只保存地址

关键点总结:

  • 闭包本质:通过堆内存延长了外部变量的生命周期
  • 栈中保存的是地址,堆中保存的是真实数据
  • 执行上下文出栈 ≠ 变量消失,只要还有引用,就继续存活

七、为什么 JS 不需要手动管理内存?

像 C/C++ 这样的语言,开发者必须使用 mallocfree 来手动分配和释放内存:

int main(){
  int a = 1;
  char* b = '极客时间';
  bool c = true;

  c = a;
  return 0;
}

而在 JavaScript 中,你完全不需要关心这些。V8 引擎自动完成内存分配和垃圾回收

  • 栈内存:执行上下文结束 → 自动弹出 → 快速回收
  • 堆内存:没有变量引用的对象 → 垃圾回收器(GC)定期清理

✅ 所以 JS 开发者可以专注于业务逻辑,而不必担心内存泄漏(虽然也要注意,比如事件监听器未解绑可能导致内存泄漏)


八、总结:JS 内存机制核心要点

内容 说明
栈内存 调用栈在其内部,存放执行上下文、简单数据类型,速度快,连续
堆内存 存放复杂对象,空间大,不连续
变量提升 编译阶段为变量预留空间,初始值为 undefined
引用传递 对象赋值是地址拷贝,多个变量共享同一对象
闭包 内部函数引用外部变量 → 堆内存保存自由变量 → 延长生命周期
栈顶指针 函数调用时移动,上下文切换高效

九、写在最后

理解 JavaScript 的内存机制,不仅能帮助我们写出更高效的代码,还能更好地掌握闭包、作用域、垃圾回收等高级概念。虽然我们不需要像 C++ 那样手动操作内存,但了解背后的原理,会让你的代码更加稳健。

💡 提醒:不要滥用闭包,因为它会让变量长期驻留内存,增加 GC 压力。合理使用,才是高手之道。


📌 如果你觉得这篇文章对你有帮助,欢迎点赞、收藏、转发!关注我,带你深入浅出地学透前端技术!

📸 图片来源:本文所有图片均为原创示意图,用于辅助理解 JS 内存模型。

❌