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。

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

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

相关推荐
竹林81826 分钟前
用 wagmi v2 + viem 监听链上事件,我踩了三天坑终于搞懂了实时日志与历史补全
javascript
Momo__30 分钟前
VueUse createReusableTemplate —— 单文件组件内的模板复用神器
前端·vue.js
只一33 分钟前
😭从回调地狱到 async/await:一文打通 Ajax 与 JS 异步编程
javascript
程序员小富37 分钟前
我开源了一个开发者专属的智能 JSON 工具,得到了媳妇高度认可
前端·vue.js·后端
小小小小宇37 分钟前
程序员如何给 LLM 装工具以及看懂推理过程
前端
写代码的皮筏艇37 分钟前
React中的forwardRef
前端·react.js·面试
槑有老呆1 小时前
花三个月工资请了个 AI 程序员,结果它连青岛啤酒股价都查不了
前端
风骏时光牛马1 小时前
Verilog开发常见问题汇总解析
前端
子兮曰1 小时前
AI Coding Method Map:一张图看懂 AI 编程的完整链路
前端·人工智能·后端