🧨 开场暴击:你写的不是代码,是内存操作!
当你写下这一行代码:
const data = users;
你以为你在“复制数据”,
但其实,你只是复制了一个指针。
而这个“指针”背后,藏着 JavaScript 运行时最核心的两大战场:
🏰 栈空间(Stack) —— 快速、有序、短命
🏜️ 堆空间(Heap) —— 广阔、混乱、持久
理解它们,才是理解“软拷贝”和“硬拷贝”的唯一正途。
🧱 第一章:内存双雄 —— 栈与堆的“社会分工”
📦 栈(Stack):程序的“前台办公室”
- 特点:自动管理、后进先出(LIFO)、访问极快
- 存放内容:
- 基本类型值:
number、string、boolean、undefined、null、symbol - 变量名(标识符)
- 函数调用帧(call stack)
- 基本类型值:
let a = 10;
let b = "hello";
let c = true;
✅ 这些变量和它们的值都直接存在 栈中。赋值时是“值拷贝”。
let d = a; // 把 10 复制一份给 d
d = 20;
console.log(a); // 依然是 10 → 完全独立
👉 因为是“值”,所以复制的是实体,不存在共享问题。
🗃️ 堆(Heap):程序的“仓库区”
- 特点:手动管理(GC 自动回收)、空间大、访问较慢
- 存放内容:
- 对象(Object)
- 数组(Array)
- 函数(Function)
- 一切复杂数据结构
const user = { name: "李德佳", city: "景德镇" };
⚠️ 注意!这行代码的真实含义是:
- 在 堆内存 中开辟一块空间,存储
{ name: "李德佳", city: "景德镇" } - 在 栈内存 中创建变量
user user存储的不是对象本身,而是指向堆中那块空间的 地址(引用)
🧠 类比:
user就像房产证,房子在“堆”里,房产证在“栈”里。
⚔️ 第二章:软拷贝的真相 —— “影子分身术”
现在我们来看这段“经典翻车代码”:
const users = [
{ id: 1, name: "李德佳" },
{ id: 2, name: "小红" }
];
const data = users; // 软拷贝
data[0].hobbies = ["篮球", "看烟花"];
🔍 内存层面发生了什么?
| 步骤 | 栈(Stack) | 堆(Heap) |
|---|---|---|
1. 创建 users | users → 地址 A | 地址 A: [ {...}, {...} ] |
2. 软拷贝 data = users | data → 地址 A(和 users 一样!) | 同一块数组对象 |
3. 修改 data[0].hobbies | 无变化 | 直接修改地址 A 中的对象 |
🎯 结果:users 和 data 共享同一个堆内存对象。改一个,等于改了两个。
❗ 软拷贝的本质:只复制栈中的引用,不碰堆里的数据。
🔄 常见软拷贝方式(都是“表面兄弟”)
// 方式1:直接赋值
const data = users;
// 方式2:扩展运算符(数组)
const data = [...users];
// 方式3:Object.assign
const data = Object.assign({}, obj);
// 方式4:slice
const data = arr.slice();
📌 它们都能生成“新变量”,但里面的嵌套对象依然指向原堆内存!
const obj = { user: { name: "德佳" } };
const shallow = { ...obj };
shallow.user.name = "小明";
console.log(obj.user.name); // ❌ 输出:"小明"
🤯 因为
shallow.user和obj.user都指向堆中同一个{ name: "德佳" }对象!
🛠️ 第三章:硬拷贝的真相 —— “克隆人计划”
要实现真正的独立,必须:
✅ 在堆内存中开辟全新的空间,把原对象的所有内容逐层复制过去。
这就是 深拷贝(Deep Copy)。
const data = JSON.parse(JSON.stringify(users));
🔍 内存层面发生了什么?
| 步骤 | 栈(Stack) | 堆(Heap) |
|---|---|---|
1. JSON.stringify(users) | 序列化 | 把堆中对象转成字符串 |
2. JSON.parse(...) | 解析 | 在堆中新建对象,填充数据 |
3. 赋值给 data | data → 新地址 B | 地址 B: 一份完全独立的副本 |
🎯 结果:data 和 users 指向不同的堆内存区域,互不影响。
✅ 硬拷贝的本质:递归遍历 + 堆内存重建
🧬 第四章:深拷贝是如何“钻地心”的?
🌀 递归拷贝算法(简化版)
function deepClone(target) {
// 基础类型,直接返回
if (typeof target !== 'object' || target === null) {
return target;
}
// 初始化结果
const result = Array.isArray(target) ? [] : {};
// 遍历所有属性
for (let key in target) {
if (target.hasOwnProperty(key)) {
// 如果属性值仍是对象,递归拷贝
result[key] = deepClone(target[key]);
}
}
return result;
}
🧠 执行过程就像一棵树的遍历:
root
/ \
user settings
/ \ |
name age theme
深拷贝会逐层进入每个节点,在堆中创建新对象,最终生成一棵结构相同但内存独立的新树。
⚠️ 第五章:为什么 JSON 方法不完美?因为它不懂“内存哲学”!
JSON.parse(JSON.stringify(obj)) 是最常用的深拷贝技巧,但它有三大致命缺陷:
| 缺陷 | 原因 | 示例 |
|---|---|---|
| ❌ 无法处理函数 | JSON 不支持函数序列化 | func: () => {} → 消失 |
❌ 丢失 undefined | JSON 忽略 undefined | name: undefined → 键被删除 |
| ❌ 循环引用报错 | 序列化时陷入无限循环 | obj.self = obj → TypeError |
🧠 根本原因:
JSON是一种数据交换格式,它只关心“可序列化的纯数据”,对内存中的“引用关系”一无所知。
🧰 第六章:现代解决方案 —— 从内存层面破局
✅ 方案1:Lodash cloneDeep —— 内存级深拷贝大师
const _ = require('lodash');
const safeCopy = _.cloneDeep(original);
🔹 原理:
- 使用
WeakMap记录已拷贝对象,避免循环引用 - 手动处理函数、正则、日期、Map/Set 等特殊类型
- 真正实现“内存隔离”
✅ 方案2:structuredClone() —— 浏览器原生的深拷贝 API
const cloned = structuredClone(originalObject);
✅ 支持:
- Date、RegExp、Map、Set、Blob、FileList、ArrayBuffer
- 循环引用(不会爆栈!)
- 跨线程传输(Worker 间通信)
🚫 不支持:
- 函数
undefinedSymbol
🚀 这是未来!它直接在 C++ 层面实现了安全的深拷贝,性能远超 JS 实现。
🧠 第七章:灵魂升华 —— 拷贝的本质是“所有权转移”
我们可以从操作系统角度重新理解:
| 类型 | 所有权模型 | 类比 |
|---|---|---|
| 软拷贝 | 共享所有权(Shared Ownership) | 多人共用一把钥匙 |
| 硬拷贝 | 独占所有权(Unique Ownership) | 我有自己的房本 |
Rust 语言就用这套模型彻底解决了内存安全问题。
而在 JavaScript 中,虽然没有“所有权”概念,但我们可以通过拷贝策略模拟:
- 只读访问 → 共享(软拷贝)
- 写操作 → 独占(硬拷贝)
🎯 终极总结:一张图看懂内存中的拷贝
💬 互动时间
欢迎在评论区讨论:
- 你有没有因为“误以为是深拷贝”导致线上事故?
- 你的项目中用
structuredClone了吗?体验如何? - 你觉得 JavaScript 未来应该内置一个
Object.deepClone()吗?
👇 点赞 + 收藏 + 分享,让更多人看清“复制”背后的内存真相!