深拷贝 vs 浅拷贝:从堆栈内存到 JSON 序列化的底层真相
“我改了副本,怎么原数据也变了?”
这是每个前端开发者都踩过的坑。
本文将带你穿透 JavaScript 的内存模型,彻底搞懂 浅拷贝、深拷贝的本质差异,并手写一个生产可用的深拷贝函数。
一、问题重现:为什么“复制”会失败?
看这段代码:
const users = [
{ id: 1, name: "Alice", hometown: "南昌" },
{ id: 2, name: "Bob", hometown: "深圳" }
];
// ❌ 看似“复制”,实则“共享”
const data = users;
data[0].name = "Charlie";
console.log(users[0].name); // "Charlie" —— 原数组被意外修改!
为什么?
因为 data = users 并没有创建新数据,而是让 data 指向了 users 在堆内存中的同一块地址。
二、内存模型:栈 vs 堆,值 vs 引用
要理解拷贝,先搞清 JS 的内存分配机制:
| 内存区域 | 存储内容 | 特点 |
|---|---|---|
| 栈(Stack) | 基本类型(number, string, boolean 等)、变量名、引用地址 | 快速分配,大小固定 |
| 堆(Heap) | 引用类型(object, array, function)的实际数据 | 动态分配,可大可小 |
📌 关键结论:
- 基本类型:赋值时直接复制值(独立);
- 引用类型:赋值时复制的是堆内存地址(共享)。
let a = 10;
let b = a; // b = 10(值拷贝)
b = 20;
console.log(a); // 10 → 不受影响
let obj1 = { x: 1 };
let obj2 = obj1; // obj2 指向 obj1 的地址
obj2.x = 2;
console.log(obj1.x); // 2 → 被修改!
三、浅拷贝:只复制“第一层”
浅拷贝会创建新对象,但嵌套的引用类型仍共享地址。
✅ 常见浅拷贝方法:
// 1. 展开运算符
const shallow1 = [...users];
// 2. Object.assign
const shallow2 = Object.assign([], users);
// 3. Array.prototype.slice
const shallow3 = users.slice();
⚠️ 浅拷贝的问题:
shallow1[0].hometown = "北京";
console.log(users[0].hometown); // "北京" —— 嵌套对象仍被共享!
💡 浅拷贝适用场景:对象只有一层结构,无嵌套引用类型。
四、深拷贝:彻底切断联系
深拷贝会递归复制所有层级,确保新旧对象完全独立。
方法1:JSON.parse(JSON.stringify())(简单但有缺陷)
const deep = JSON.parse(JSON.stringify(users));
deep[0].hobbies = ["篮球", "看烟花"];
console.log(users[0].hobbies); // undefined → 完全隔离!
❌ 它的致命缺陷:
| 数据类型 | 结果 |
|---|---|
undefined / Symbol / Function | 丢失 |
Date | 变成字符串 "2025-01-01T..." |
RegExp | 变成空对象 {} |
| 循环引用 | 报错:Converting circular structure to JSON |
Map / Set / WeakMap | 无法正确复制 |
✅ 仅适用于纯 JSON 数据(对象/数组 + 基本类型)。
五、手写深拷贝:生产级实现
我们需要一个能处理各种边界的递归函数:
function deepClone(obj, hash = new WeakMap()) {
// 处理 null 和非对象
if (obj === null || typeof obj !== "object") return obj;
// 处理 Date
if (obj instanceof Date) return new Date(obj);
// 处理 RegExp
if (obj instanceof RegExp) return new RegExp(obj);
// 防止循环引用
if (hash.has(obj)) return hash.get(obj);
// 初始化克隆对象
const clone = Array.isArray(obj) ? [] : {};
// 缓存引用,防止循环
hash.set(obj, clone);
// 递归拷贝所有属性
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
clone[key] = deepClone(obj[key], hash);
}
}
return clone;
}
✅ 支持:
- 循环引用(通过
WeakMap缓存) Date、RegExp- 数组与对象
- 自定义属性(
hasOwnProperty过滤原型链)
🧪 测试:
const original = {
name: "Test",
date: new Date(),
regex: /\d+/g,
self: null // 将指向自己
};
original.self = original; // 循环引用
const cloned = deepClone(original);
console.log(cloned.date instanceof Date); // true
console.log(cloned.regex instanceof RegExp); // true
console.log(cloned.self === cloned); // true(且不报错!)
六、终极方案:用 Lodash
在真实项目中,别重复造轮子:
import _ from 'lodash';
const deep = _.cloneDeep(users);
Lodash 的 cloneDeep 经过充分测试,支持:
- 所有内置类型(
Map,Set,Promise,Error等) - 循环引用
- 自定义 clone 函数
七、总结:何时用哪种拷贝?
| 场景 | 推荐方案 |
|---|---|
| 简单对象,无嵌套 | 浅拷贝({...obj}) |
| 纯 JSON 数据 | JSON.parse(JSON.stringify()) |
| 复杂对象(含函数、日期等) | Lodash _.cloneDeep |
| 学习/面试 | 手写递归深拷贝(带循环引用处理) |
记住:
- 浅拷贝 = 表面独立,内里相连;
- 深拷贝 = 彻底分离,互不干扰。
理解内存模型,才能写出健壮的代码。
附:快速自查
- 我的数据有嵌套对象吗?
- 是否包含函数、日期、正则?
- 是否可能存在循环引用?
- 我真的需要深拷贝,还是状态管理更合适?(如 Redux、Vue Reactivity)
本文代码均可直接运行。
下次再遇到“改副本影响原数据”,你就知道该往哪看了!