← 返回题目列表

JavaScript 如何实现深拷贝?JSON 方案有什么问题?

高频 中等 第 13 / 29 题 更新于 2026/07/27
JavaScript深拷贝对象

简化版

深拷贝是递归复制对象内部的所有引用结构,让新对象和旧对象互不影响。JSON.parse(JSON.stringify(obj)) 简单但会丢失函数、undefined、Symbol、Date、RegExp、循环引用等信息。现代环境可用 structuredClone,复杂场景需要自定义递归并处理循环引用和特殊对象。

详细版

浅拷贝只复制第一层:

const a = { user: { name: 'Tom' } };
const b = { ...a };
b.user.name = 'Jerry';
console.log(a.user.name); // Jerry

深拷贝要复制嵌套对象。基础实现:

function clone(obj, map = new WeakMap()) {
  if (obj === null || typeof obj !== 'object') return obj;
  if (map.has(obj)) return map.get(obj);
  const res = Array.isArray(obj) ? [] : {};
  map.set(obj, res);
  for (const key of Reflect.ownKeys(obj)) {
    res[key] = clone(obj[key], map);
  }
  return res;
}

WeakMap 用来解决循环引用。

完整版教学

一、为什么浅拷贝不够

对象属性如果仍然指向同一个子对象,那么第一层复制后,嵌套数据仍然共享。表单初始值、状态快照、撤销重做等场景里,这会导致修改新对象时污染旧对象。扩展运算符与 Object.assign 都只复制一层属性值;当属性值是对象引用时,复制的是引用本身而不是其后代结构。

二、JSON 方案的局限

JSON 方案只能处理 JSON 能表达的数据。它会忽略 undefined、函数、Symbol key,对 Date 会变成字符串,对 RegExp 会变成普通对象,遇到循环引用会直接报错。

const obj = { a: undefined, date: new Date(), fn() {} };
JSON.parse(JSON.stringify(obj));

这个结果已经不是原对象的真实结构。

三、structuredClone 的优势和边界

浏览器和 Node 新版本支持 structuredClone,能处理很多内置类型和循环引用:

const copy = structuredClone(obj);

但它不能克隆函数和 DOM 节点,也不是所有运行环境都有。业务里要考虑兼容性。

四、自定义深拷贝要处理什么

更完整的深拷贝需要考虑:

  • 基本类型直接返回。
  • 数组和普通对象递归复制。
  • DateRegExpMapSet 特殊处理。
  • 循环引用用 WeakMap 缓存。
  • Symbol key 用 Reflect.ownKeys

面试中写出基础递归和 WeakMap,通常已经能覆盖核心。

五、面试追问与工程落地

深拷贝常会追问“能不能拷贝原型和属性描述符”。基础递归只复制可枚举属性和值,不会完整保留 getter/setter、不可枚举属性、原型链、属性描述符。如果要做库级深拷贝,需要用 Object.getOwnPropertyDescriptorsObject.create(Object.getPrototypeOf(obj)) 等更完整地复制结构。

还要注意深拷贝不一定是最优方案。大对象频繁深拷贝会很耗性能。状态管理里常用结构共享,只复制变化路径,未变化子树继续复用引用。Immer 这类工具就是在可变写法和不可变结果之间做平衡。

工程判断是:普通接口纯数据可以 structuredClone 或 JSON 方案;复杂对象用成熟库;性能敏感状态更新不要无脑深拷贝全量对象。

六、深拷贝本质上是复制对象图

对象结构可能不是一棵树,而是带共享节点和环的图。一个含 3个对象节点、4条引用边的例子里,若 source.a === source.b,正确的图复制通常还应满足 copy.a === copy.b,同时 copy.a !== source.a;如果递归时每遇到一次都创建新对象,就会破坏共享身份。WeakMap 的作用不仅是阻止循环引用无限递归,也是在克隆图中复用已经创建的目标节点。

source ─┬─ a ──> node ──> source
        └─ b ─────┘
copy   ─┬─ a ──> node2 ─> copy
        └─ b ─────┘
数据JSON 往返structuredClone基础手写递归
DateRegExp日期常变字符串,正则丢结构支持必须分支处理
MapSet通常变空普通对象支持必须递归键和值
循环引用抛错支持依赖缓存表
函数、DOM 节点函数被忽略,DOM 信息不可靠DataCloneError需定义业务语义
属性描述符、原型不保留通常不按原对象语义完整复制可定制但成本高

以含 undefined 的对象和数组为例:对象属性可能被 JSON 序列化省略,而数组槽位会变成 nullNaNInfinity 也会序列化成 null。因此“JSON 会丢 undefined”还不够,数据所在位置会导致不同结果。

在实现前先定义“深拷贝”的语义:是否保留共享引用、原型、描述符、访问器和可转移资源;不存在适合所有 JavaScript 值的单一递归模板。

七、常见误区与追问

  • 误区:递归遍历所有属性就能得到通用深拷贝。 循环图、共享引用、内置类型、访问器、原型与描述符都会让简单递归失真。
  • 误区:structuredClone 可以复制任何 JavaScript 值。 函数和部分宿主对象不可结构化克隆,遇到不可序列化成员会抛 DataCloneError
  • 误区:JSON 方案只存在循环引用一个问题。 它还会改变或丢失 undefined、Symbol、函数、非有限数、Date、Map、Set 等语义。
  • 追问:为什么缓存表要在递归子属性前写入? 若对象直接或间接指回自身,必须先登记源对象与目标对象的映射,递归遇环时才能立即复用。
  • 追问:structuredClone 的 transfer 有什么作用? ArrayBuffer 等可转移对象能把底层资源所有权移给结果,避免复制成本,但原对象中的资源会被分离而不可再用。
  • 追问:深拷贝时间复杂度是多少? 对可遍历对象图通常至少访问每个节点和边一次,可近似看作 O(V + E),额外映射空间约为 O(V)
  • 追问:状态管理为什么更偏向结构共享? 只复制发生变化的路径可以保留未变化子树引用,降低全量遍历、内存分配和依赖比较成本。

八、加强记忆

深拷贝不是“把对象复制一下”这么简单,难点在嵌套引用、特殊对象和循环引用。JSON 方案只适合纯数据对象;通用场景优先想 structuredClone 或可靠工具函数。