JavaScript 深拷贝对象:structuredClone、JSON 与递归方案完整对比详解

编程狮(w3cschool.cn) 2026-09-08 14:57:59 浏览数 (19)
反馈

JavaScript 深拷贝优先考虑 structuredClone;只处理纯 JSON 数据时可以用序列化;需要保留函数、原型或自定义规则时再写递归方案。真正的关键不是“复制得更深”,而是先确认数据里有哪些类型。

JavaScript 深拷贝三种方案封面图

你复制了一个配置对象,改完副本里的主题颜色,原对象也跟着变了。这通常不是框架出错,而是两个变量仍然引用同一层嵌套数据。本文基于现代浏览器与 Node.js 运行环境,比较 structuredClone、JSON 序列化和递归复制三种 JavaScript 深拷贝方案,并用 Date、Map、循环引用等边界案例说明怎么选。今天这篇文章,编程狮帮你把“能复制”和“复制正确”分开讲清。

一、JavaScript 深拷贝解决的是什么问题

对象变量保存的不是整份对象,而是指向对象的引用。使用展开语法或 Object.assign 时,只会创建最外层的新对象;嵌套对象仍然共享同一个引用,这叫浅拷贝。

const original = {
  user: { name: "w3cschool", skills: ["HTML", "JavaScript"] }
};

const shallow = { ...original };
shallow.user.name = "编程狮";

console.log(original.user.name); // 编程狮

虽然 shallow 本身是新对象,但 shallow.user 和 original.user 指向同一对象,所以修改会串到原数据。第一次接触引用和值的区别,可以先过一遍 JavaScript 基础教程

JavaScript 深拷贝要递归复制整张对象图,让副本中的可变数据不再与原对象共享。这里特意说“对象图”,因为真实数据可能存在循环引用,也可能让两个属性共同指向同一对象。一个合格方案还要决定如何处理 Date、RegExp、Map、Set、ArrayBuffer、函数和类实例。

二、structuredClone:现代环境的默认选择

structuredClone 是现代浏览器和较新 Node.js 中提供的原生深复制能力。它支持普通对象、数组、Date、RegExp、Map、Set、ArrayBuffer,以及循环引用,比 JSON 方案覆盖的类型更完整。

const source = {
  createdAt: new Date("2026-09-08T08:00:00Z"),
  tags: new Set(["JavaScript", "clone"]),
  profile: { name: "w3cschool" }
};
source.self = source;

const copy = structuredClone(source);
copy.profile.name = "编程狮";

console.log(source.profile.name);       // w3cschool
console.log(copy.createdAt instanceof Date); // true
console.log(copy.self === copy);        // true

这段代码保留了 Date、Set 和循环结构,同时切断了 profile 的共享引用。structuredClone 不能复制函数、DOM 节点等不可结构化克隆的值,遇到这些内容会抛出 DataCloneError。

如果运行环境比较旧,应先检查兼容范围或通过功能检测决定降级方案。不要只根据语法是否报错来判断数据正确,因为某些类型可能被悄悄转换。

三、JSON 序列化:只适合纯数据对象

JSON.parse(JSON.stringify(value)) 写法短,适合由字符串、数字、布尔值、null、数组和普通对象构成的数据。接口响应、表单初始值和简单缓存快照经常满足这个条件。

const source = {
  name: "w3cschool",
  level: 3,
  enabled: true,
  lessons: ["HTML", "CSS"]
};

const copy = JSON.parse(JSON.stringify(source));
copy.lessons.push("JavaScript");

console.log(source.lessons.length); // 2
console.log(copy.lessons.length);   // 3

这段 JavaScript 深拷贝在纯 JSON 数据上足够直观,但它会把 Date 变成字符串,忽略 undefined、Symbol 和函数,无法处理 BigInt 与循环引用,NaN 和 Infinity 也会失真。关于 JSON 数据格式,可以继续阅读 JSON 文件与 JavaScript 示例

⚠️ 注意:JSON 序列化不是通用深拷贝。只要数据模型里出现非 JSON 类型,就应该明确测试转换结果,而不是默认“没报错就是对”。

四、递归复制:为特殊规则保留控制权

手写递归方案适合两类场景:一是你必须兼容没有 structuredClone 的环境;二是业务需要自定义规则,例如函数保持引用、类实例调用自己的复制方法,或某些字段故意跳过。

下面给出一个支持常见对象、数组、Date、RegExp、Map、Set 和循环引用的最小版本:

function deepClone(value, seen = new WeakMap()) {
  if (value === null || typeof value !== "object") return value;
  if (seen.has(value)) return seen.get(value);
  if (value instanceof Date) return new Date(value.getTime());
  if (value instanceof RegExp) return new RegExp(value.source, value.flags);

  if (value instanceof Map) {
    const result = new Map();
    seen.set(value, result);
    for (const [key, item] of value) {
      result.set(deepClone(key, seen), deepClone(item, seen));
    }
    return result;
  }

  if (value instanceof Set) {
    const result = new Set();
    seen.set(value, result);
    for (const item of value) result.add(deepClone(item, seen));
    return result;
  }

  const result = Array.isArray(value)
    ? []
    : Object.create(Object.getPrototypeOf(value));
  seen.set(value, result);

  for (const key of Reflect.ownKeys(value)) {
    result[key] = deepClone(value[key], seen);
  }
  return result;
}

WeakMap 用来记录已经复制过的对象,它既能阻止无限递归,也能保持共享引用关系。这个示例仍未完整复制属性描述符、私有字段、WeakMap 和平台对象,因此不能包装成“万能函数”。

五、3 种 JavaScript 深拷贝方案怎么选

方案 适合场景 支持循环引用 主要限制
structuredClone 现代环境中的一般数据 支持 不复制函数和部分平台对象
JSON 序列化 确认是纯 JSON 的数据 不支持 类型丢失、BigInt 报错
自定义递归 有特殊类型或业务规则 可支持 维护和测试成本高

选择时先回答三个问题:运行环境是否有 structuredClone,数据是否严格符合 JSON,是否存在必须自定义的复制规则。绝大多数新项目先用 structuredClone;接口纯数据可以用 JSON;只有前两者明确不满足时,才值得维护递归实现。

常见误区还包括用深拷贝掩盖状态设计问题。大型状态树每次完整复制会增加时间和内存消耗。如果只修改一条路径,可以考虑不可变更新、结构共享或只复制受影响的层级。

5.1 用边界测试证明复制结果

不要只测试 {a: 1}。至少准备嵌套数组、Date、Map、undefined、循环引用和共享引用六类样本。每次复制后同时检查值相等与引用不相等,还要验证修改副本不会影响原对象。

const shared = { score: 100 };
const source = { left: shared, right: shared };
const copy = structuredClone(source);

console.assert(copy !== source);
console.assert(copy.left !== source.left);
console.assert(copy.left === copy.right);

copy.left.score = 60;
console.assert(source.left.score === 100);

最后一组断言验证了两个容易遗漏的事实:副本与原对象隔离,同时副本内部原有的共享关系仍被保留。需要临时查数组与对象 API 时,JavaScript 速查手册 比反复猜方法名更可靠。

JavaScript 深拷贝方案选择决策树

总结

JavaScript 深拷贝没有一种写法适合所有数据。structuredClone 是现代环境的优先方案;JSON 序列化适合经过确认的纯数据;自定义递归则用于兼容或业务规则明确的场景。选择之前先列出真实类型,选择之后用边界样本验证结果。

记住三点:展开语法只有一层;JSON 会改变非 JSON 类型;深拷贝还要保持循环和共享引用关系。下一步可以把项目里的复制场景列出来,用本文的选择表逐个替换没有类型依据的“万能写法”。

延伸学习

想继续练习对象与模块,可以按这个顺序:

  1. 先学 JavaScript 原生模块课程,把对象、导出与工程组织串起来;
  2. 再读 Object.assign 拷贝边界,巩固浅拷贝与深拷贝的区别;
  3. 最后用 JavaScript 基础实战 编写并测试自己的数据处理模块。

常见问题

Q:展开运算符能做 JavaScript 深拷贝吗?

不能通用地做到。展开语法只复制最外层属性,嵌套对象和数组仍共享引用。数据只有一层时看起来有效,但结构一旦加深,就可能出现修改副本影响原对象的问题。

Q:structuredClone 会复制函数吗?

不会。函数属于不可结构化克隆的值,传入后通常会抛出 DataCloneError。如果业务必须保留函数,应明确保持原引用或设计自定义复制规则。

Q:JSON 深拷贝为什么会丢 undefined?

因为 undefined 不是 JSON 支持的数据类型。对象属性中的 undefined 会被忽略,数组里的 undefined 通常会变成 null,所以只能在纯 JSON 数据上使用这套方案。

Q:深拷贝一定比浅拷贝好吗?

不是。深拷贝成本更高,也可能破坏原型或特殊对象语义。只修改少量层级时,针对受影响路径做浅拷贝往往更清晰,也更容易控制性能。

0 人点赞