从值拷贝到深拷贝:彻底弄懂 JavaScript 的堆与栈
彻底搞懂 JS 的堆内存与栈内存:从值拷贝到深拷贝
在学习 JavaScript 的过程中,经常会遇到一些令人困惑的问题:
- 为什么我改了一个对象,另一个变量也变了?
- 为什么有的赋值是“独立”的,有的却会互相影响?
- 深拷贝和浅拷贝,到底区别在哪?
这些现象的根源,其实都来自 —— 堆内存(Heap)与栈内存(Stack) 的不同存储机制。
本文将带你从内存模型出发,搞懂数据的“居住位置”和“传递方式”,彻底弄清值拷贝与引用拷贝的差异。
一、栈内存与堆内存:存储机制的区别
JavaScript 会根据变量类型,选择不同的存储方式:
| 数据类型 | 存储位置 | 特点 |
|---|---|---|
| 基本类型(Number、String、Boolean、null、undefined、Symbol、BigInt) | 栈内存(Stack) | 连续存储、读写高效、空间固定 |
| 引用类型(Object、Array、Function) | 堆内存(Heap) | 动态分配、可扩展、访问间接 |
栈内存:简单变量的“快递柜”
let a = 1;
let b = 2;
let c = 3;
let d = a; // 值拷贝
栈内存中存储的是值本身,且空间连续。d = a 实际上是对 1 这个值的复制。
因此,无论之后如何修改 a,都不会影响到 d。
关键特征:
每个变量都有自己的小空间,互不干扰、读取极快。
堆内存:对象与数组的“仓库区”
const users = [
{ id: 1, name: "oumasyu", hometown: "赣州" },
{ id: 2, name: "inx177", hometown: "南昌" },
{ id: 3, name: "gustt_", hometown: "赣州" }
];
数组和对象属于引用类型。
它们的实际数据存放在堆内存中,而变量 users 只是保存了一个引用地址。
当你执行:
const data = users;
data[0].hobbies = ["篮球", "看烟花"];
console.log(data, users);
结果会发现 —— users 也被改动了!
这是因为:
-
users和data在栈中存放的地址相同; - 它们同时指向堆内存中同一块数据区域;
- 改变任何一方,其实都在修改那块共享的堆空间。
二、引用式拷贝:看似复制,实则共用
可以把这种情况理解为:
users ──► [ { id:1, name:"oumasyu" } ]
▲
│(共用同一地址)
data ┘
data 和 users 并没有创建两份数据,只是共用一个引用。
所以,修改 data[0] 的属性,等同于修改 users[0]。
三、想要真正“分家”?你需要深拷贝
如果希望两个对象互不影响,就必须让它们在堆内存中拥有各自的空间。
方法一:JSON.parse(JSON.stringify())
const data = JSON.parse(JSON.stringify(users));
data[0].hobbies = ["篮球", "看烟花"];
console.log(data, users);
执行后你会发现:
修改 data 不会再影响 users —— 它们终于“分家”了。
原理:
通过序列化和反序列化,将对象转成字符串再重新生成,从而创建一份全新的数据结构。
优点:简单直接、常用于深拷贝。
缺点:无法拷贝函数、undefined、Symbol、循环引用等。
方法二:structuredClone()(更现代)
const data = structuredClone(users);
structuredClone() 是浏览器原生的深拷贝 API。
相比 JSON 方法,它支持更多数据类型(如 Date、RegExp、Map、Set、循环引用等),
是未来更推荐的写法。
四、图解内存变化:从共享到独立
# 引用式拷贝
users ──► [ { id:1, name:"oumasyu" } ]
▲
│
data ┘ (共用同一堆空间)
# 深拷贝后
users ──► [ { id:1, name:"oumasyu" } ]
data ──► [ { id:1, name:"oumasyu", hobbies:["篮球"] } ]
(独立的两份堆内存数据)
五、核心对比总结
| 拷贝类型 | 是否新建堆内存 | 是否共享数据 | 常见实现方式 |
|---|---|---|---|
| 值拷贝(基本类型) | 是 | 否 | = |
| 引用拷贝(对象/数组) | 否 | 是 | = |
| 深拷贝 | 是 | 否 |
JSON.parse(JSON.stringify()) / structuredClone()
|
总结
理解堆内存与栈内存的本质,是写好 JS 的关键一步。
当你清楚变量“指的是什么”,你就能轻松判断:
- 哪些修改会相互影响;
- 何时该用深拷贝;
- 如何优化内存和性能。
一句话总结:
基本类型复制的是值,引用类型复制的是地址。
想要真正“断开关系”,就得创建新的堆内存。