深拷贝 vs 浅拷贝:从堆栈内存到 JSON 序列化的底层真相

4 阅读3分钟

深拷贝 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 缓存)
  • DateRegExp
  • 数组与对象
  • 自定义属性(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)

本文代码均可直接运行。
下次再遇到“改副本影响原数据”,你就知道该往哪看了!