JavaScript 深拷贝完全指南:structuredClone() 与其他克隆方法的全面对比

原创2026-10-02 21:22:53357 阅读
文章标签:教程文档

JavaScript 深拷贝完全指南:structuredClone() 与其他克隆方法的全面对比

JavaScript 中的对象克隆是极易踩坑的场景:浅拷贝会在嵌套数据上"漏馅",JSON.stringify() 会丢弃函数与特殊类型,手写递归深拷贝也可能遗漏细节。本文围绕 30-seconds-of-code 仓库中的同名技术文章展开,系统讲解 structuredClone() 的原理、边界行为与适用场景,并通过逐项对比 shallowClone、deepClone、JSON 序列化三种方案,帮助读者在真实项目中做出正确的克隆选型。

说明:下文引用的示例代码与对比结论均来自 deep-clone-structured-clone.md 原文,仓库相关路径均已转换为以仓库根目录为起点的相对链接。


一、克隆方法总览:四种方案的定位差异

在深入 structuredClone() 之前,先看四种常见克隆方案的核心实现与定位差异。以下是原文给出的四种典型实现:

const shallowClone = obj => ({ ...obj });

const deepClone = obj => {
  if (obj === null) return null;
  let clone = Object.assign({}, obj);
  Object.keys(clone).forEach(
    key =>
      (clone[key] =
        typeof obj[key] === 'object' ? deepClone(obj[key]) : obj[key])
  );
  if (Array.isArray(obj)) {
    clone.length = obj.length;
    return Array.from(clone);
  }
  return clone;
};

const jsonClone = obj => JSON.parse(JSON.stringify(obj));

四种方案的定位差异如下:

  • shallowClone:使用展开运算符创建对象的浅拷贝,嵌套对象仍是对原对象的引用;
  • deepClone:递归克隆对象,能处理嵌套对象与数组,但对特殊类型无能为力;
  • jsonClone:借助 JSON.stringify() 与 JSON.parse() 实现深拷贝,但受限于 JSON 数据类型;
  • structuredClone():JavaScript 原生内置方法,基于 结构化克隆算法,能处理其他方案无法覆盖的众多边界情况。

[!NOTE] 关于克隆方法的详细讲解,可参见仓库内的前序文章 shallow-deep-clone-object.md,该文解释了为什么对象按引用传递、浅克隆与深克隆的区别,并指出 structuredClone() 自 Node.js v17.0.0 起可用。

二、对比总表:逐项能力一览

原文给出了四方案在九大特性上的完整对比表,这是理解 structuredClone() 价值与局限的核心依据:

Feature shallowClone deepClone JSON structuredClone
Nested objects/arrays ⚠️ ✅ ✅ ✅
Functions ⚠️ ⚠️ ❌ ❌ Error
Built-in types ⚠️ ❌[1] ❌ ✅
DOM nodes ⚠️ ❌[2] ❌ ❌ Error
Circular references ⚠️ ❌[3] ❌ Error ✅
Prototype chain ❌ ❌[4] ❌ ❌
Getters/Setters ❌ ❌[5] ❌ ❌
Symbol properties ✅ ✅ ❌ ❌
Private fields ❌ ❌ ❌ ❌

图例:✅ = 支持;❌ = 不支持;⚠️ = 引用浅拷贝;Error = 抛出异常 [1] 内置类型通常无法被正确克隆,除非专门处理 [2] 克隆 DOM 节点行为不一致,且因实现而异 [3] 循环引用会抛出异常,除非专门处理 [4] 原型链能否保留取决于具体实现 [5] 属性描述符可以保留,若显式处理

从上表可以清晰看到:structuredClone() 是唯一能同时正确处理嵌套数据、内置类型与循环引用的内置方案,但它同样不保留原型链、访问器、Symbol 属性与私有字段。下文逐项展开这些边界情况。

三、逐项边界情况剖析

以下逐一分析各特殊场景下 structuredClone() 与其他方案的行为差异(嵌套对象与数组除外,因为除浅拷贝外其余方案行为一致)。

3.1 函数(Functions):不支持克隆

structuredClone() 与 JSON.stringify() 都不会克隆函数。浅拷贝与自定义深拷贝仅复制函数引用,而不克隆函数本身——这意味着克隆对象仍引用原函数,在配合闭包或 this 上下文时可能引发意外行为。

const obj = { fn: () => 1 };

shallowClone(obj).fn === obj.fn; // true
deepClone(obj).fn === obj.fn; // true
jsonClone(obj).fn; // null
structuredClone(obj); // throws DataCloneError

相关主题可参考 closures.md。

3.2 内置类型(Built-in types):structuredClone 的强项

Date、Map、Set、RegExp 等内置对象无法被 JSON.stringify() 正确克隆——它们会被转成字符串或空对象。浅拷贝复制引用,自定义深拷贝除非专门处理否则也会失败。而 structuredClone() 能正确处理这些类型,在克隆时保留其结构与属性。

[!WARNING] 结构化克隆算法对 Error 类型对象的处理略有不同,可查阅 MDN 官方文档 了解详情。

const obj = {
  date: new Date('2025-06-07'),
  map: new Map([['key', 'value']]),
  set: new Set([1, 2, 3]),
  regexp: /abc/g,
};

shallowClone(obj).date === obj.date; // true (Date object, same for others)
deepClone(obj).date; // Empty object (same for others)
jsonClone(obj).date; // String representation (the rest are empty objects)
structuredClone(obj).date === obj.date; // true (Date object, same for others)

[!IMPORTANT] 几乎所有原生类型都有基于构造函数的直截了当的克隆方案。如果无法使用 structuredClone(),可以在自定义深拷贝函数中为它们添加专门的处理逻辑。

3.3 DOM 节点(DOM nodes):明确抛错

structuredClone() 无法克隆 DOM 节点,尝试克隆会抛出 DataCloneError。这符合直觉——克隆 DOM 节点意味着要在文档中创建新节点。其他方案虽然不会抛错,但处理得同样不优雅(返回空对象或空节点),静默失败可能反而更糟,相关讨论见 vocal-fails-silencing-errors.md。

const obj = { node: document.querySelector('body') };

shallowClone(obj).node === obj.node; // true
deepClone(obj).node; // Empty DOM node
jsonClone(obj).node; // Empty object
structuredClone(obj); // throws DataCloneError

3.4 循环引用(Circular references):structuredClone 的招牌能力

structuredClone() 开箱即用地支持循环引用。JSON.stringify() 会抛出异常,大多数自定义深拷贝函数若不专门设计也会失败。

const obj = {};
obj.self = obj;

shallowClone(obj).self === obj; // true
deepClone(obj).self; // throws RangeError: Maximum call stack size exceeded
JSON.stringify(obj); // throws TypeError: Converting circular structure to JSON
structuredClone(obj).self === obj; // false

对于必须用 JSON 序列化循环引用的场景,可参考仓库文章 stringify-circular-json.md——它用 WeakSet 追踪已访问对象、配合自定义 replacer 过滤循环引用,从而避免 TypeError(代价是序列化结果会丢失循环数据)。

3.5 原型链(Prototype chain):最令人意外的短板

structuredClone() 最令人意外的一点是它不保留原型链。无论原对象的原型是什么,克隆对象的原型始终是 Object.prototype。不过其他三种方案同样不保留原型链,除非自定义深拷贝函数显式处理。

const proto = { x: 1 };
const obj = Object.create(proto);

shallowClone(obj).x; // undefined
deepClone(obj).x; // undefined
jsonClone(obj).x; // undefined
structuredClone(obj).x; // undefined

原型与继承的相关背景可参考 classical-vs-prototypal-inheritance.md。

3.6 存取器(Getters/Setters):只克隆当前值

所有方案(包括 structuredClone())都不保留 getter/setter,只克隆当前值。克隆对象不再具备原对象的访问器行为。概括来说,任何方案都不会保留属性描述符等元数据类特性,除非在自定义深拷贝函数中显式处理。

const obj = {
  valueField: 42,
  get value() {
    return this.valueField;
  },
  set value(val) {
    this.valueField = val;
  },
};
typeof obj.value; // object (getter/setter)

typeof shallowClone(obj).value; // number (independent values)
typeof deepClone(obj).value; // number (independent values)
typeof jsonClone(obj).value; // number (independent values)
typeof structuredClone(obj).value; // number (independent values)

若需要精确控制属性描述符,可参考 add-key-value-pair-to-object.md 中关于 Object.defineProperty() 的用法。

3.7 Symbol 属性(Symbol properties):被忽略

structuredClone() 不克隆以 Symbol 为键的属性,而是像 JSON.stringify() 一样直接忽略。浅拷贝与自定义深拷贝则按引用复制 Symbol 属性。

const sym = Symbol('key');
const obj = { [sym]: 'value' };

shallowClone(obj)[sym] === obj[sym]; // true
deepClone(obj)[sym] === obj[sym]; // true
jsonClone(obj)[sym]; // undefined
structuredClone(obj)[sym]; // undefined

3.8 私有字段(Private fields):同样不克隆

私有类字段与方法同样不会被 structuredClone() 克隆。克隆结果只包含公开、可枚举的属性。这与其余方案一致——私有属性不属于对象的可枚举属性,因此都无法被复制。

class Secret {
  #hidden = 42;
  value = 7;
  get hidden() {
    return this.#hidden;
  }
};
const obj = new Secret();

shallowClone(obj); // #hidden is not cloned
deepClone(obj); // #hidden is not cloned
jsonClone(obj); // #hidden is not cloned
structuredClone(obj); // #hidden is not cloned

四、性能考量

structuredClone() 是内置方法,通常针对性能做过优化。尤其在处理含循环引用或内置类型的复杂对象时,它快于自定义深拷贝函数和 JSON.stringify()。

一个实用的经验法则:只要 structuredClone() 能满足你的使用场景,它的性能大概率优于任何手写方案。再加上随着 JavaScript 引擎不断优化,其性能还有持续提升的空间——这使它成为深拷贝的可靠选择。

五、何时使用 structuredClone()

structuredClone() 是原生、无负担地深克隆 JavaScript 对象的强大工具。只要不存在自定义深拷贝才能处理的特殊边界需求,都应优先选用 structuredClone()——它快速、安全,并能处理诸多会击穿其他方案的边界情况。

以下场景可以考虑改用自定义深拷贝函数:

  • 需要保留原对象的原型链;
  • 需要克隆函数或 DOM 节点;
  • 需要保留属性描述符、getter 或 setter;
  • 需要保留 Symbol 属性。

即便如此,也常常可以围绕 structuredClone() 编写一个薄封装:在保留其性能与可靠性的同时,只为所需场景补充额外处理逻辑。

[!WARNING] 结构化克隆算法可能在本文未覆盖的场景抛出 DataCloneError,尤其是平台特定对象或类型。务必针对自己的用例进行测试。支持类型的完整列表可查阅 MDN 关于 Structured clone algorithm 的页面。

六、结语

structuredClone() 是 JavaScript 中强大而通用的深拷贝方法。它解决了其他方案难以处理的诸多边界问题——循环引用、内置类型、复杂数据结构等。尽管存在不保留原型链、无法克隆函数等局限,它依然是绝大多数日常场景的最佳选择。

进一步阅读

围绕克隆与对象操作的更多实战文章,可直接在仓库中继续阅读:

此外,仓库的 javascript.yaml 语言资源页中收录了 structuredClone() 的 MDN 文档链接,可作为进一步查阅的入口。

登录后查看全文
30-seconds-of-code