堆内存 vs 栈内存:JavaScript 中的“内存双雄”大对决

49 阅读4分钟

堆内存 vs 栈内存:JavaScript 中的“内存双雄”大对决


引子:你以为你只是在写代码?其实你在玩“内存分配”!

当你写下 let a = 1;const users = [...] 的时候,你可能以为自己只是在定义变量。但其实——你的代码正在悄悄地向计算机申请一块“地盘”,这块地盘要么在栈内存(Stack) ,要么在堆内存(Heap)

今天,我们就来一场“内存双雄”的深度剖析,带你搞懂 JavaScript 中堆与栈的本质区别、动态性、引用式拷贝 vs 值拷贝,以及那个让人又爱又恨的深拷贝


一、栈内存:高效稳定的“公务员”

特点:

  • 连续存储
  • 读写快如闪电
  • 大小固定(基本类型值大小确定,无法动态扩容)
  • 存放基本数据类型numberstringbooleanundefinednull 等
  • 对于对象/数组,栈中只存一个引用地址(指针) ,指向堆中的真实数据

示例代码:

let a = 1;
let b = 2;
let c = 3;
let d = a; // 值拷贝,相当于复印了一份

这里,abcd 都是简单变量,它们的值直接存在栈里。当你执行 d = a,其实是把 1 这个值“复印”了一份给 d。后续无论你怎么改 da 都纹丝不动。

💡 小幽默:栈内存就像一个纪律严明的公务员办公室——每个人都有固定工位,不准乱动,效率极高,但想临时加个椅子?门都没有!


二、堆内存:自由奔放的“创业公司”

特点:

  • 由垃圾回收器管理,整体布局非连续(但单个对象通常占一块连续空间)
  • 支持动态扩容
  • 存的是复杂数据结构:数组、对象、函数等
  • 变量本身(在栈中)只保存一个地址指针,指向堆中的真实数据

示例代码:

const users = [
    { id: 1, name: '陈', hometown: '江西' },
    { id: 2, name: '徐',   hometown: '湖南' },
    { id: 3, name: '王', hometown: '湖北' }
];

// 动态性!堆内存支持弹性扩容
users.push({
    id: 4,
    name: '赵',
    hometown: '广东'
});

看!我们轻松往 users 数组里加了一个新用户。这就是堆内存的动态性——它可以根据需求自动扩展空间,像极了创业公司:人来了就加工位,灵活得很!

🚨 但注意:users 这个变量本身(在栈中)只是一个“门牌号”,真正数据住在堆里的“豪宅”中。


三、引用式拷贝:你以为是复制,其实是“共享账号”

现在问题来了:

const data = users; // 引用式拷贝!
data[0].hobbies = ['篮球', '看烟花'];
console.log(data, users);

输出你会发现:users 也被改了!

为什么?因为 data = users 并没有复制数据,而是把 users 的“门牌号”(堆地址)给了 data。两者指向同一个堆内存对象——就像你和室友共用一个《王者荣耀》账号,你辛辛苦苦练到王者50星,结果他半夜登录,拿你的李白去打人机还送了20个人头……第二天你一上线,战绩惨不忍睹,心态直接炸裂!

💥 这就是引用拷贝的“共享悲剧”:一人操作,全员背锅。


四、深拷贝:真正的“独立户口本”

那怎么才能真正复制一份,互不影响呢?

经典方案:JSON.parse(JSON.stringify(...))

// 先序列化成字符串(脱离原对象),再反序列化成新对象
var data = JSON.parse(JSON.stringify(users));
data[0].hobbies = ['篮球', '看烟花'];
console.log(data, users); // users 不受影响!

这个操作相当于:

  1. 把堆里的对象“拍成照片”(JSON 字符串)
  2. 拿这张照片去堆里重新建一栋一模一样的房子(新对象)
  3. data 指向新房子,和 users 再无瓜葛

优点:简单、有效(对纯 JSON 数据)
缺点

  • 会丢失函数、undefinedSymbol
  • DateRegExp 等对象会被转成字符串或空对象
  • 无法处理循环引用(直接报错)
  • 原型链信息全部丢失

🔧 进阶提示:生产环境建议用 lodash.cloneDeep 或手写递归深拷贝函数。


五、变量声明的“玄学”:var 与类型推断

var users; // undefined,占栈内存一个小格子
var data;  // 同上

// 后面赋值为数组 → 类型由值决定!
users = [ /* ... */ ]; // 此时 users 指向堆内存

JavaScript 是动态类型语言,变量本身没有类型,类型由它当前的值决定。users 最初是 undefined(栈),后来变成对象引用(还是栈,但指向堆)。

🧠 大佬思考题:如果 users 被重新赋值为 null,它还指向堆吗?
答案:不指向。null 是基本类型值,直接存储在栈中,不关联任何堆内存。


六、总结:一张表看懂堆 vs 栈

特性栈内存(Stack)堆内存(Heap)
存储内容基本类型的值、对象/数组的引用地址(指针)对象、数组、函数等复杂数据结构
内存分配时机函数调用时自动分配,作用域结束自动释放运行时动态分配,由垃圾回收器管理
访问速度极快(连续内存 + 直接访问)较慢(需通过指针跳转)
生命周期随函数调用结束而销毁不确定,依赖引用关系和 GC(垃圾回收)
拷贝行为值拷贝(独立副本)默认引用拷贝(共享地址)
适合场景临时变量、函数参数、基本类型操作大型数据、需要动态增删改的结构