JavaScript 深拷贝:如何彻底切断引用关联?

🧬 JavaScript 深拷贝:如何彻底切断引用关联?

在 JavaScript 中,赋值操作对于引用类型 (Object, Array 等)只是复制了内存地址 (指针)。

这意味着两个变量指向堆内存中的同一个对象

  • 浅拷贝(Shallow Copy):只复制第一层属性。如果属性值是对象,则复制其引用。
  • 深拷贝(Deep Copy) :递归复制所有层级。在堆内存中开辟一块全新的空间,存放完全独立的数据副本。

📂 目录

  1. [🤔 为什么需要深拷贝?](#🤔 为什么需要深拷贝?)
  2. [⚡ 方案一:JSON 序列化(最快但有限制)](#⚡ 方案一:JSON 序列化(最快但有限制))
  3. [🛠️ 方案二:结构化克隆 API structuredClone(现代首选)](#🛠️ 方案二:结构化克隆 API structuredClone(现代首选))
  4. [💻 方案三:手写递归深拷贝(面试必考,最灵活)](#💻 方案三:手写递归深拷贝(面试必考,最灵活))
  5. [📚 方案四:第三方库 Lodash](#📚 方案四:第三方库 Lodash)
  6. [⚠️ 常见陷阱与注意事项](#⚠️ 常见陷阱与注意事项)
  7. [💡 总结](#💡 总结)

1. 🤔 为什么需要深拷贝?

先看一个经典的"事故现场":

javascript 复制代码
const original = {
  name: "Lingma",
  info: {
    age: 1,
    skills: ["JS", "Vue"],
  },
};

// ❌ 错误做法:直接赋值或浅拷贝
const copy = { ...original }; // 或者 Object.assign({}, original)

// 修改副本中的嵌套对象
copy.info.age = 99;

console.log(original.info.age); // 99 😱 原数据也被修改了!

原因... 展开运算符只复制了 original 的第一层。info属性是一个对象,复制的是它的引用地址copy.infooriginal.info 指向堆内存中的同一个对象

深拷贝的目标 :让 copyoriginal 在内存中完全独立,互不影响。


2. ⚡ 方案一:JSON 序列化(最快但有限制)

这是最简单、最常用的"一行代码"深拷贝方法。

💻 代码实现

javascript 复制代码
const original = { a: 1, b: { c: 2 } };
const copy = JSON.parse(JSON.stringify(original));

copy.b.c = 99;
console.log(original.b.c); // 2 ✅ 互不影响

✅ 优点

  • 极简:无需引入库,无需手写逻辑。
  • 快速:浏览器底层 C++ 优化,性能通常优于手写递归。

❌ 缺点(致命缺陷)

它会丢失转换以下数据类型:

  1. undefined:会被忽略。
  2. Function:会被忽略。
  3. Symbol:会被忽略。
  4. Date:会变成字符串(ISO 格式),不再是 Date 对象。
  5. RegExp :会变成空对象 {}
  6. NaN, Infinity :会变成 null
  7. 循环引用 :直接报错 TypeError: Converting circular structure to JSON

适用场景:纯数据对象(POJO),不包含函数、日期、正则等特殊类型。


3. 🛠️ 方案二:结构化克隆 API structuredClone(现代首选)

ES2022 引入的全局 API,旨在解决 JSON 方法的局限性。

💻 代码实现

javascript 复制代码
const original = {
  date: new Date(),
  map: new Map([["key", "value"]]),
  set: new Set([1, 2]),
  nested: { a: 1 },
};

const copy = structuredClone(original);

copy.nested.a = 99;
console.log(original.nested.a); // 1 ✅

console.log(copy.date instanceof Date); // true ✅ (JSON 方法做不到)

✅ 优点

  • 原生支持:无需 polyfill(现代浏览器已支持)。
  • 类型保留:支持 Date, Map, Set, RegExp, ArrayBuffer 等。
  • 处理循环引用:不会报错,能正确处理环形结构。

❌ 缺点

  • 不支持函数:依然无法拷贝 Function。
  • 兼容性:旧版浏览器(如 IE)不支持,需检查环境。

适用场景:现代项目,需要拷贝包含 Date/Map/Set 但不含函数的复杂对象。


4. 💻 方案三:手写递归深拷贝(面试必考,最灵活)

在面试中,面试官通常会要求你手写一个深拷贝函数,以考察你对递归、类型判断和边界情况的处理。

🧠 核心思路

  1. 判断类型:如果是基本类型,直接返回。
  2. 处理特殊对象:Date, RegExp 等需特殊构造。
  3. 处理循环引用 :使用 WeakMap 缓存已拷贝的对象,防止死循环。
  4. 递归拷贝:遍历对象/数组的每个属性,递归调用自身。

💻 代码实现

javascript 复制代码
function deepClone(obj, hash = new WeakMap()) {
  // 1. 处理 null 或非对象类型
  if (obj === null || typeof obj !== "object") {
    return obj;
  }

  // 2. 处理循环引用:如果已经拷贝过,直接返回缓存
  if (hash.has(obj)) {
    return hash.get(obj);
  }

  // 3. 处理特殊内置对象
  if (obj instanceof Date) {
    return new Date(obj.getTime());
  }
  if (obj instanceof RegExp) {
    return new RegExp(obj.source, obj.flags);
  }

  // 如果需要支持 Map/Set,可在此添加逻辑
  // if (obj instanceof Map) { ... }
  // if (obj instanceof Set) { ... }

  // 4. 创建新容器(保持原型链)
  // 使用 Object.create 可以保留原型链,比 {} 或 [] 更严谨
  const cloneObj = Array.isArray(obj)
    ? []
    : Object.create(Object.getPrototypeOf(obj));

  // 5. 存入缓存,防止循环引用
  hash.set(obj, cloneObj);

  // 6. 递归拷贝所有属性
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      cloneObj[key] = deepClone(obj[key], hash);
    }
  }

  return cloneObj;
}

🧪 测试循环引用

javascript 复制代码
const a = { val: 1 };
a.self = a; // 循环引用

const b = deepClone(a);
console.log(b.val); // 1
console.log(b.self === b); // true ✅ 正确保持了内部引用关系

✅ 优点

  • 可控:你可以决定如何处理函数、Symbol、DOM 节点等。
  • 兼容性好:可在任何 JS 环境中运行。
  • 面试加分项:展示了对 WeakMap、原型链和递归的深刻理解。

❌ 缺点

  • 性能较差:递归开销大,深层嵌套对象可能栈溢出。
  • 代码复杂:需要考虑很多边界情况。

5. 📚 方案四:第三方库 Lodash

在生产环境中,如果项目已经引入了 Lodash,直接使用 _.cloneDeep 是最稳妥的选择。

javascript 复制代码
import _ from "lodash";

const copy = _.cloneDeep(original);

✅ 优点

  • 健壮:经过多年社区验证,处理了绝大多数边缘情况。
  • 省心:无需自己维护拷贝逻辑。

❌ 缺点

  • 包体积 :如果只为这一个功能引入整个 Lodash,会增加打包体积(建议使用 lodash.clonedeep 按需引入)。

6. ⚠️ 常见陷阱与注意事项

  1. 函数无法被深拷贝

    函数是执行代码的逻辑,不是数据。通常深拷贝会忽略函数,或者你只能拷贝函数的字符串表示(fn.toString()),但这失去了闭包上下文。

  2. DOM 节点不能深拷贝

    DOM 节点包含大量浏览器内部状态,不能用 JSON 或普通递归拷贝。如需拷贝 DOM,请使用原生 API node.cloneNode(true)

  3. 性能考量

    深拷贝是非常昂贵的操作。对于大型数据(如几万条列表),频繁深拷贝会导致页面卡顿。

    • 优化建议 :尽量使用不可变数据(Immutable Data)思维,只在必要时拷贝;或使用结构性共享(如 Immutable.js, Immer)。
  4. Getter/Setter 丢失

    普通的递归拷贝可能会丢失属性的 get/set 描述符。如果需要保留,需使用 Object.getOwnPropertyDescriptorObject.defineProperty


💡 总结

方案 优点 缺点 推荐指数 适用场景
JSON 简单、快 丢失函数/Date/undefined,不支持循环引用 ⭐⭐⭐ 纯数据对象,后端交互数据
structuredClone 原生、支持多种类型、支持循环引用 不支持函数,兼容性需注意 ⭐⭐⭐⭐⭐ 现代浏览器环境首选
手写递归 灵活、可定制、面试必备 性能一般,代码复杂 ⭐⭐⭐⭐ 面试、特殊定制需求
Lodash 稳健、功能全 增加包体积 ⭐⭐⭐⭐ 已使用 Lodash 的项目

🚀 博主寄语

深拷贝不是银弹。

在现代前端开发中,我们更推崇**不可变数据(Immutability)**的理念。

与其每次修改都深拷贝,不如使用 Immer 这样的库,通过 Proxy 代理来实现"看似可变,实则不可变"的高效状态管理。

记住选型原则

  1. 简单数据用 JSON
  2. 现代环境用 structuredClone
  3. 复杂定制手写信。
  4. 生产环境靠 Lodash 或 Immer。

希望这篇文档能帮你彻底掌握深拷贝!如果有疑问,欢迎在评论区留言。👇

喜欢这篇文章吗?记得点赞、收藏、转发哦! ❤️

相关推荐
知识分享小能手1 小时前
R语言入门学习教程,从入门到精通,初识R语言(1)
开发语言·学习·r语言
代码羊羊2 小时前
Rust 迭代器完全通俗易懂指南(零基础全覆盖)
java·开发语言·rust
MY_TEUCK9 小时前
【Java 后端】SpringBoot 登录认证与会话跟踪实战(JWT + Filter/Interceptor)
java·开发语言·spring boot
镜宇秋霖丶9 小时前
2026.5.6@霖宇博客制作中遇见的问题
前端·javascript·vue.js
QQ24221997910 小时前
基于python+微信小程序的家教管理系统_mh3j9
开发语言·python·微信小程序
沐知全栈开发10 小时前
JavaScript 条件语句
开发语言
RSTJ_162510 小时前
PYTHON+AI LLM DAY THREETY-SEVEN
开发语言·人工智能·python
吴声子夜歌10 小时前
Vue3——TypeScript基础
javascript·typescript
清水白石00810 小时前
《Python性能深潜:从对象分配开销到“小对象风暴”的破解之道(含实战与最佳实践)》
开发语言·python