JavaScript 内存揭秘:堆(Heap) vs 栈(Stack)

🧠 JavaScript 内存揭秘:堆(Heap) vs 栈(Stack)

在 JavaScript 引擎(如 V8)中,内存主要分为两个区域:栈内存(Stack)堆内存(Heap)

它们就像公司的办公桌仓库,分工明确,协作高效。

📂 目录

  1. [🏢 核心比喻:办公桌 vs 仓库](#🏢 核心比喻:办公桌 vs 仓库)
  2. [📦 栈(Stack):快速、有序、自动清理](#📦 栈(Stack):快速、有序、自动清理)
  3. [🏭 堆(Heap):庞大、无序、手动回收](#🏭 堆(Heap):庞大、无序、手动回收)
  4. [💻 代码实战:数据是如何存储的?](#💻 代码实战:数据是如何存储的?)
  5. [🔄 垃圾回收:谁在打扫战场?](#🔄 垃圾回收:谁在打扫战场?)
  6. [💡 总结与面试考点](#💡 总结与面试考点)

1. 🏢 核心比喻:办公桌 vs 仓库

为了通俗易懂,我们把浏览器内存想象成一家公司:

📌 栈(Stack) = 员工的办公桌

  • 特点:空间小,但就在手边,存取速度极快。
  • 用途:存放正在处理的临时数据(如函数调用、局部变量)。
  • 管理:老板(JS 引擎)严格管理。你离开座位(函数执行结束),桌子上的东西立刻被清空。
  • 结构:先进后出(LIFO),像叠盘子一样。

📌 堆(Heap) = 公司的公共仓库

  • 特点:空间巨大,但距离远,存取速度相对较慢。
  • 用途:存放大件物品、复杂对象(如大数组、复杂对象、闭包)。
  • 管理:比较松散。东西扔进去就不管了,直到仓库满了,清洁工(垃圾回收器 GC)才会来清理没人用的东西。
  • 结构:无序,像一个巨大的杂物间,需要通过"地址标签"才能找到东西。

2. 📦 栈(Stack):快速、有序、自动清理

✅ 存储内容

  • 基本数据类型Number, String, Boolean, Null, Undefined, Symbol, BigInt
  • 执行上下文:函数调用栈(Call Stack),记录当前执行到哪里了。

⚙️ 工作机制

  1. 分配:当声明一个变量或调用一个函数时,内存会自动在栈顶分配一块固定大小的空间。
  2. 释放 :当函数执行完毕或变量超出作用域时,这块内存会自动弹出并释放。
  3. 速度:极快,因为不需要查找,直接操作栈顶指针。

注意:在 JS 中,短字符串有时也会存储在栈中(取决于引擎优化),但逻辑上我们将其视为值类型。


3. 🏭 堆(Heap):庞大、无序、手动回收

✅ 存储内容

  • 引用数据类型Object, Array, Function, Date, RegExp 等。
  • 大型数据:无论数据多大,都存放在堆中。

⚙️ 工作机制

  1. 分配:当创建一个对象时,引擎会在堆中开辟一块空间存放数据。
  2. 引用 :栈中只存储一个指针(内存地址),指向堆中的这块数据。
  3. 释放不会自动立即释放 。需要依靠浏览器的垃圾回收机制(Garbage Collection, GC) 来定期扫描,找出不再被引用的对象并清除。

4. 💻 代码实战:数据是如何存储的?

让我们通过代码看看栈和堆是如何协作的。

场景一:基本类型(全在栈中)

javascript 复制代码
let a = 10;
let b = a; // 拷贝值
b = 20;

console.log(a); // 10
console.log(b); // 20

内存图解

text 复制代码
Stack (栈)
+-------+
| b: 20 |  <-- 修改 b,不影响 a
+-------+
| a: 10 |  <-- a 保持原值
+-------+

结论 :基本类型是值拷贝。互不影响。

场景二:引用类型(栈存地址,堆存数据)

javascript 复制代码
let obj1 = { name: "Lingma" };
let obj2 = obj1; // 拷贝地址(指针)
obj2.name = "Aliyun";

console.log(obj1.name); // "Aliyun" 😱 obj1 也被改变了!
console.log(obj2.name); // "Aliyun"

内存图解

text 复制代码
Stack (栈)                  Heap (堆)
+----------+              +------------------+
| obj1     |------------->| { name: "Aliyun" } |
+----------+              +------------------+
| obj2     |-------------^  (同一个对象)
+----------+

结论 :引用类型是引用拷贝 (浅拷贝)。obj1obj2 指向堆中的同一个对象。修改其中一个,另一个也会变。


5. 🔄 垃圾回收:谁在打扫战场?

既然堆内存不会自动释放,那什么时候清理呢?

浏览器使用 垃圾回收机制(GC) 。主流算法是 标记-清除(Mark-and-Sweep)

🧹 工作流程

  1. 标记 :GC 从根节点(如 window、全局变量)出发,遍历所有能访问到的对象,打上"存活"标记。
  2. 清除 :遍历堆内存,那些没有被打上标记的对象,说明已经没有任何变量引用它们了,于是被判定为"垃圾",占用内存被释放。

⚠️ 内存泄漏(Memory Leak)

如果代码中存在意外的全局变量未清理的定时器循环引用,导致某些对象永远无法被 GC 标记为"可回收",内存就会越占越多,最终导致页面卡顿甚至崩溃。

常见泄漏场景

javascript 复制代码
// 1. 意外全局变量
function leak() {
  leakedVar = "I am global now"; // 忘记写 let/var/const
}

// 2. 未清除的定时器
const timer = setInterval(() => {
  console.log("I never stop");
}, 1000);
// 如果不清除 clearInterval(timer),回调函数及其引用的变量永远不会被回收

6. 💡 总结与面试考点

特性 栈(Stack) 堆(Heap)
存储内容 基本类型、执行上下文 引用类型(对象、数组等)
大小 小,固定大小 大,动态分配
存取速度
管理方式 自动分配与释放(LIFO) 垃圾回收机制(GC)
数据结构 线性,有序 非线性,无序
拷贝行为 值拷贝(深拷贝效果) 引用拷贝(浅拷贝)

🎯 面试高频问答

Q1: 为什么基本类型赋值互不影响,而对象赋值会相互影响?

A: 因为基本类型存在栈中,赋值是复制值;对象存在堆中,栈里只存地址,赋值是复制地址,两者指向同一块堆内存。

Q2: 什么是深拷贝?如何实现?

A : 深拷贝是在堆中开辟一块新内存,将原对象的所有层级数据完整复制一份。实现方式:JSON.parse(JSON.stringify(obj))(有局限)、递归复制、或使用 structuredClone API。

Q3: 闭包会导致内存泄漏吗?

A: 不一定。闭包会阻止外部变量被回收,这是正常现象。但如果闭包长期存在且引用了巨大的无用数据,且无法被 GC 回收,才会导致泄漏。
🚀 博主寄语

理解堆和栈,不仅仅是为了应付面试。

当你遇到"修改了一个对象,另一个莫名其妙也变了"的 Bug 时,你会想起堆内存的共享特性

当你发现页面越来越卡时,你会想起堆内存的垃圾回收

记住口诀

栈小快,存基本,自动清理不费力。

堆大慢,存对象,引用拷贝要注意。

垃圾回收靠标记,内存泄漏要警惕。

希望这篇文档能帮你彻底搞懂堆和栈的区别!如果有疑问,欢迎在评论区留言。👇

喜欢这篇文章吗?记得点赞、收藏、转发哦! ❤️

相关推荐
lbb 小魔仙16 分钟前
基于Python构建RAG(检索增强生成)系统:从原理到企业级实战
开发语言·python
代码的小搬运工36 分钟前
UITableView
开发语言·ui·ios·objective-c
刚子编程39 分钟前
C# Join 深度解析:参数顺序、多表关联与空值处理最佳实践
开发语言·c#·最佳实践·join·多表关联·空值处理
AbandonForce40 分钟前
哈希表(HashTable,散列表)个人理解
开发语言·数据结构·c++·散列表
代码中介商1 小时前
栈结构完全指南:顺序栈实现精讲
c语言·开发语言·数据结构
平凡但不平庸的码农1 小时前
Go 错误处理详解
开发语言·后端·golang
z200509301 小时前
C++中位图和布隆过滤器的一些面试题
开发语言·c++
Bat U1 小时前
JavaEE|文件操作和IO
java·开发语言
脉动数据行情1 小时前
Python 实现融通金行情数据对接(实时推送 + K 线 + 产品列表)
开发语言·python
花归去2 小时前
vue3中 function getText(){} 、 const getText=()=>{} ;区别在哪里,优缺点
javascript·vue.js·ecmascript