从值拷贝到深拷贝:彻底弄懂 JavaScript 的堆与栈

彻底搞懂 JS 的堆内存与栈内存:从值拷贝到深拷贝

在学习 JavaScript 的过程中,经常会遇到一些令人困惑的问题:

  • 为什么我改了一个对象,另一个变量也变了?
  • 为什么有的赋值是"独立"的,有的却会互相影响?
  • 深拷贝和浅拷贝,到底区别在哪?

这些现象的根源,其实都来自 ------ 堆内存(Heap)与栈内存(Stack) 的不同存储机制。

本文将带你从内存模型出发,搞懂数据的"居住位置"和"传递方式",彻底弄清值拷贝与引用拷贝的差异。


一、栈内存与堆内存:存储机制的区别

JavaScript 会根据变量类型,选择不同的存储方式:

数据类型 存储位置 特点
基本类型(Number、String、Boolean、null、undefined、Symbol、BigInt) 栈内存(Stack) 连续存储、读写高效、空间固定
引用类型(Object、Array、Function) 堆内存(Heap) 动态分配、可扩展、访问间接

栈内存:简单变量的"快递柜"

ini 复制代码
let a = 1;
let b = 2;
let c = 3;
let d = a; // 值拷贝

栈内存中存储的是值本身 ,且空间连续。
d = a 实际上是对 1 这个值的复制。

因此,无论之后如何修改 a,都不会影响到 d。

关键特征:

每个变量都有自己的小空间,互不干扰、读取极快。


堆内存:对象与数组的"仓库区"

bash 复制代码
const users = [
  { id: 1, name: "oumasyu", hometown: "赣州" },
  { id: 2, name: "inx177", hometown: "南昌" },
  { id: 3, name: "gustt_", hometown: "赣州" }
];

数组和对象属于引用类型 。

它们的实际数据存放在堆内存中,而变量 users 只是保存了一个引用地址。

当你执行:

ini 复制代码
const data = users;
data[0].hobbies = ["篮球", "看烟花"];
console.log(data, users);

结果会发现 ------ users 也被改动了!

这是因为:

  • users 和 data 在栈中存放的地址相同;
  • 它们同时指向堆内存中同一块数据区域;
  • 改变任何一方,其实都在修改那块共享的堆空间。

二、引用式拷贝:看似复制,实则共用

可以把这种情况理解为:

bash 复制代码
users ──► [ { id:1, name:"oumasyu" } ]
   ▲
   │(共用同一地址)
data ┘

data 和 users 并没有创建两份数据,只是共用一个引用。

所以,修改 data[0] 的属性,等同于修改 users[0]。


三、想要真正"分家"?你需要深拷贝

如果希望两个对象互不影响 ,就必须让它们在堆内存中拥有各自的空间。

方法一:JSON.parse(JSON.stringify())

ini 复制代码
const data = JSON.parse(JSON.stringify(users));
data[0].hobbies = ["篮球", "看烟花"];
console.log(data, users);

执行后你会发现:

修改 data 不会再影响 users ------ 它们终于"分家"了。

原理 :

通过序列化和反序列化,将对象转成字符串再重新生成,从而创建一份全新的数据结构。

优点:简单直接、常用于深拷贝。

缺点:无法拷贝函数、undefined、Symbol、循环引用等。


方法二:structuredClone()(更现代)

ini 复制代码
const data = structuredClone(users);

structuredClone() 是浏览器原生的深拷贝 API。

相比 JSON 方法,它支持更多数据类型(如 Date、RegExp、Map、Set、循环引用等),

是未来更推荐的写法。


四、图解内存变化:从共享到独立

bash 复制代码
# 引用式拷贝
users ──► [ { id:1, name:"oumasyu" } ]
   ▲
   │
data ┘  (共用同一堆空间)

# 深拷贝后
users ──► [ { id:1, name:"oumasyu" } ]
data  ──► [ { id:1, name:"oumasyu", hobbies:["篮球"] } ]
(独立的两份堆内存数据)

五、核心对比总结

拷贝类型 是否新建堆内存 是否共享数据 常见实现方式
值拷贝(基本类型) 是 否 =
引用拷贝(对象/数组) 否 是 =
深拷贝 是 否 JSON.parse(JSON.stringify()) / structuredClone()

总结

理解堆内存与栈内存的本质,是写好 JS 的关键一步。

当你清楚变量"指的是什么",你就能轻松判断:

  • 哪些修改会相互影响;
  • 何时该用深拷贝;
  • 如何优化内存和性能。

一句话总结:

基本类型复制的是值,引用类型复制的是地址。

想要真正"断开关系",就得创建新的堆内存。

相关推荐
miss14 分钟前
AI 改完不敢上线?OpenSpec 规约驱动开发实战
前端·代码规范·前端工程化
AiMan1 小时前
腾讯ima还是不可以导出左侧所有对话,只能单条导出,方法不对吗
前端
去伪存真1 小时前
并发量一高就装死?Agent 全链路“防猝死”手册
前端·agent
Moment1 小时前
AI 编程时代,聊聊我的 Mac mini 双屏桌面
前端·后端
AiMan1 小时前
先确认能不能一次把Microsofat Copilot所有内容打包一个文件导出
前端
总要学点什么1 小时前
第97篇:三维数据加载-城市建筑白模自定义着色器——让一条光带沿高度扫上去,快慢却由帧率说了算
前端·gis·webgl·cesium·着色器·空间分析·白模
上海懒图科技2 小时前
工厂厂区电子地图定位导航:厂区人车精准定位,安全合规双保障
前端·arcgis·vue
西安小哥2 小时前
端侧跑大模型,真正的门槛不在参数量:一次 596 秒到 10 秒的压缩路径复盘
前端·人工智能
陈随易2 小时前
Cloudflare 出钱,Vite 团队挑人,44 个维护者分 30 万美元
前端·后端·程序员
恋猫de小郭3 小时前
Flutter THLC 的 WebView 滚动终于从 70 FPS 提升到 120 FPS
android·前端·flutter