🧠 JavaScript 内存揭秘:堆(Heap) vs 栈(Stack)
在 JavaScript 引擎(如 V8)中,内存主要分为两个区域:栈内存(Stack) 和 堆内存(Heap) 。
它们就像公司的办公桌 和仓库,分工明确,协作高效。
📂 目录
- [🏢 核心比喻:办公桌 vs 仓库](#🏢 核心比喻:办公桌 vs 仓库)
- [📦 栈(Stack):快速、有序、自动清理](#📦 栈(Stack):快速、有序、自动清理)
- [🏭 堆(Heap):庞大、无序、手动回收](#🏭 堆(Heap):庞大、无序、手动回收)
- [💻 代码实战:数据是如何存储的?](#💻 代码实战:数据是如何存储的?)
- [🔄 垃圾回收:谁在打扫战场?](#🔄 垃圾回收:谁在打扫战场?)
- [💡 总结与面试考点](#💡 总结与面试考点)
1. 🏢 核心比喻:办公桌 vs 仓库
为了通俗易懂,我们把浏览器内存想象成一家公司:
📌 栈(Stack) = 员工的办公桌
- 特点:空间小,但就在手边,存取速度极快。
- 用途:存放正在处理的临时数据(如函数调用、局部变量)。
- 管理:老板(JS 引擎)严格管理。你离开座位(函数执行结束),桌子上的东西立刻被清空。
- 结构:先进后出(LIFO),像叠盘子一样。
📌 堆(Heap) = 公司的公共仓库
- 特点:空间巨大,但距离远,存取速度相对较慢。
- 用途:存放大件物品、复杂对象(如大数组、复杂对象、闭包)。
- 管理:比较松散。东西扔进去就不管了,直到仓库满了,清洁工(垃圾回收器 GC)才会来清理没人用的东西。
- 结构:无序,像一个巨大的杂物间,需要通过"地址标签"才能找到东西。
2. 📦 栈(Stack):快速、有序、自动清理
✅ 存储内容
- 基本数据类型 :
Number,String,Boolean,Null,Undefined,Symbol,BigInt。 - 执行上下文:函数调用栈(Call Stack),记录当前执行到哪里了。
⚙️ 工作机制
- 分配:当声明一个变量或调用一个函数时,内存会自动在栈顶分配一块固定大小的空间。
- 释放 :当函数执行完毕或变量超出作用域时,这块内存会自动弹出并释放。
- 速度:极快,因为不需要查找,直接操作栈顶指针。
注意:在 JS 中,短字符串有时也会存储在栈中(取决于引擎优化),但逻辑上我们将其视为值类型。
3. 🏭 堆(Heap):庞大、无序、手动回收
✅ 存储内容
- 引用数据类型 :
Object,Array,Function,Date,RegExp等。 - 大型数据:无论数据多大,都存放在堆中。
⚙️ 工作机制
- 分配:当创建一个对象时,引擎会在堆中开辟一块空间存放数据。
- 引用 :栈中只存储一个指针(内存地址),指向堆中的这块数据。
- 释放 :不会自动立即释放 。需要依靠浏览器的垃圾回收机制(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 |-------------^ (同一个对象)
+----------+
结论 :引用类型是引用拷贝 (浅拷贝)。
obj1和obj2指向堆中的同一个对象。修改其中一个,另一个也会变。
5. 🔄 垃圾回收:谁在打扫战场?
既然堆内存不会自动释放,那什么时候清理呢?
浏览器使用 垃圾回收机制(GC) 。主流算法是 标记-清除(Mark-and-Sweep)。
🧹 工作流程
- 标记 :GC 从根节点(如
window、全局变量)出发,遍历所有能访问到的对象,打上"存活"标记。 - 清除 :遍历堆内存,那些没有被打上标记的对象,说明已经没有任何变量引用它们了,于是被判定为"垃圾",占用内存被释放。
⚠️ 内存泄漏(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))(有局限)、递归复制、或使用structuredCloneAPI。
Q3: 闭包会导致内存泄漏吗?
A: 不一定。闭包会阻止外部变量被回收,这是正常现象。但如果闭包长期存在且引用了巨大的无用数据,且无法被 GC 回收,才会导致泄漏。
🚀 博主寄语 :理解堆和栈,不仅仅是为了应付面试。
当你遇到"修改了一个对象,另一个莫名其妙也变了"的 Bug 时,你会想起堆内存的共享特性 。
当你发现页面越来越卡时,你会想起堆内存的垃圾回收。
记住口诀 :
栈小快,存基本,自动清理不费力。
堆大慢,存对象,引用拷贝要注意。
垃圾回收靠标记,内存泄漏要警惕。
希望这篇文档能帮你彻底搞懂堆和栈的区别!如果有疑问,欢迎在评论区留言。👇
喜欢这篇文章吗?记得点赞、收藏、转发哦! ❤️