C06 深拷贝与结构化克隆

2026-10-31 17:040 阅读约 15 分钟JavaScript

C06 深拷贝与结构化克隆

摘要:浅拷贝只复制第一层,嵌套对象仍共享引用;JSON 往返转换能断开嵌套,但会丢弃 undefined、函数、Symbol,把 Date 变成字符串,Map/Set 变成空对象,遇到循环引用直接抛错。structuredClone 使用结构化克隆算法,支持循环引用与多数内置类型,不支持函数与 DOM 节点。本文给出三种深拷贝实现的边界、类型支持对照表与深比较的两种写法。
关键词:深拷贝,结构化克隆,循环引用,深比较,不可变

所属:frontend/js / C-高级 | 上一篇:C05-JavaScript工程化 | 下一篇:C07-观察者与WebAPI | 返回:C00-总结

1 引言

「改副本污染了原数据」与「拷贝后数据变样」是同一件事的两面:前者用浅拷贝处理嵌套数据,后者用 JSON 处理含特殊类型的数据。两类缺陷都发生在拷贝方式与被拷贝内容不匹配时。

本文给出三种方式的适用边界。范围见表 1-1。

项 本文覆盖 本文不覆盖
拷贝 展开、JSON、structuredClone、手写递归 不可变数据库(Immer)
边界 循环引用、内置类型、属性描述符 跨线程传输(C07)
比较 浅比较、深比较、循环引用处理 差异化比对算法

表 1-1 本文覆盖范围

2 预备知识

项 要求 核对方式
环境 Node.js 20+(内置 structuredClone) node -e "structuredClone({})"
前置 A02、B07 引用与集合已掌握
文件 js-practice/clone.mjs 编码 UTF-8

表 2-1 环境与前置要求

3 原理与实现

3.1 三种方式的复制深度

复制深度决定嵌套对象是否共享,如图 3-1 所示。

flowchart TD
    A["源对象 source"] --> B["浅拷贝 展开/assign"]
    A --> C["JSON 往返"]
    A --> D["structuredClone"]
    B --> E["第一层独立,嵌套层共享"]
    C --> F["全部独立,但类型被重编码"]
    D --> G["全部独立,类型按需保留"]
    E --> H["改嵌套影响源对象"]
    F --> I["Date/Map/函数丢失或变形"]
    G --> J["函数与 DOM 节点抛错"]

图 3-1 三种拷贝方式的深度与副作用

图上可见:深度与保真度不可兼得,选择依据是数据里是否存在特殊类型与循环引用。

3.2 完整实现

// js-practice/clone.mjs;运行环境:Node.js 20+
const source = {
  name: 'Lin',
  when: new Date('2026-10-31T00:00:00Z'),
  tags: ['a', 'b'],
  meta: new Map([['k', 1]]),
  nothing: undefined,
  fn: () => 1,
};

// 1 浅拷贝:嵌套层仍共享
const shallow = { ...source, tags: source.tags.slice(0, 1) };
source.tags.push('c');
console.log(shallow.tags);                 // ['a'],已手工断开一层

// 2 JSON 往返:类型被重编码
const viaJson = JSON.parse(JSON.stringify(source));
console.log(viaJson.when instanceof Date); // false,是字符串
console.log(viaJson.meta instanceof Map);  // false,是空对象
console.log('nothing' in viaJson);         // false,undefined 被丢弃
console.log(typeof viaJson.fn);            // undefined

// 3 structuredClone:保留内置类型,不支持函数
const plain = {
  name: 'Lin',
  when: new Date('2026-10-31T00:00:00Z'),
  tags: ['a', 'b'],
  meta: new Map([['k', 1]]),
  nested: { deep: { n: 1 } },
};
const cloned = structuredClone(plain);
console.log(cloned.when instanceof Date);  // true
console.log(cloned.meta instanceof Map);   // true
cloned.nested.deep.n = 2;
console.log(plain.nested.deep.n);          // 1,完全独立

// 4 循环引用:JSON 抛错,structuredClone 正常
const cyclic = { self: null };
cyclic.self = cyclic;
try {
  JSON.stringify(cyclic);
} catch (err) {
  console.log(err.name);                   // TypeError
}
const cyclicClone = structuredClone(cyclic);
console.log(cyclicClone.self === cyclicClone); // true,引用结构被保留

// 5 手写递归:可控且可处理特殊规则
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);
  if (value instanceof Map) return new Map([...value].map(([k, v]) => [k, deepClone(v, seen)]));
  if (value instanceof Set) return new Set([...value].map((v) => deepClone(v, seen)));

  const out = Array.isArray(value) ? [] : Object.create(Object.getPrototypeOf(value));
  seen.set(value, out);
  for (const [key, val] of Object.entries(value)) out[key] = deepClone(val, seen);
  return out;
}

console.log(deepClone(plain).meta instanceof Map);  // true

// 6 深比较:先比引用再比结构
function deepEqual(a, b) {
  if (a === b) return true;
  if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) return false;
  const keys = Object.keys(a);
  if (keys.length !== Object.keys(b).length) return false;
  return keys.every((k) => deepEqual(a[k], b[k]));
}

console.log(deepEqual(cloned, plain));      // true
console.log(deepEqual({ a: 1 }, { a: 2 })); // false

三种方式的类型支持见表 3-1。

内容 展开 JSON structuredClone 手写递归
嵌套对象与数组 否 是 是 是
Date 共享 变字符串 保留 需专门处理
Map/Set 共享 变空对象 保留 需专门处理
RegExp/Error 共享 变空对象 保留(Error 部分) 需专门处理
undefined/函数 保留 丢弃 函数抛错 保留
循环引用 保留引用 抛 TypeError 支持 需 WeakMap
属性描述符 丢失 丢失 丢失 可保留

表 3-1 三种深拷贝方式的类型支持对照

选择依据见表 3-2。

场景 选择 理由
纯数据,需跨线程或持久化 structuredClone / JSON 结果可结构化传输
含函数或 DOM 节点 浅拷贝 + 手工处理 语义无法自动复制
需要保留描述符与原型 手写递归 唯一可控方式
状态更新(不可变) 局部展开 只复制变化的路径,成本最低

表 3-2 四种场景的选择

4 验证与讨论

  1. 执行脚本,确认 viaJson.when 不是 Date,而 cloned.when 是 Date。
  2. 在控制台执行 structuredClone({ fn: () => 1 }),确认抛出 DataCloneError。
  3. 构造 cyclic 后分别调用三种方式,确认只有 JSON 抛错。
  4. 用 deepEqual 比较 deepClone(plain) 与 plain,确认结构一致且引用不同。
检查项 预期结果 说明
浅拷贝边界 嵌套层仍共享引用 图 3-1
类型保真 Date/Map 在克隆后仍可用 表 3-1
循环引用 structuredClone 保留结构 3.2 节
函数抛错 克隆含函数对象抛 DataCloneError 表 3-1
深比较 结构相同返回 true 3.2 节

表 4-1 五项验收

讨论与局限。 structuredClone 的复制成本随对象规模线性增长,高频场景(如每帧状态更新)应改为局部展开。另,深比较对含循环引用的对象会无限递归,需在实现中加入 WeakMap 记录已比较的配对;3.2 节第 6 段的简化实现只比较可枚举属性,不比较 Date、Map、Set 的内部内容,需要精确比较时应按类型分支处理。

5 常见问题

现象 原因 处理
改副本污染原数据 用了浅拷贝 改用 structuredClone 或逐层展开
拷贝后 Date 变成字符串 走了 JSON 往返 改用 structuredClone
Converting circular structure JSON 遇到循环引用 改用 structuredClone 或手写带 WeakMap
DataCloneError 对象含函数或 DOM 节点 先剔除这些字段再克隆
Map 拷贝后变成空对象 走了 JSON 表 3-1 选 structuredClone
深比较卡死 存在循环引用 用 WeakMap 记录已比较配对

表 5-1 深拷贝的六类问题

6 结论

序号 结论 依据
1 浅拷贝只断开第一层 图 3-1
2 JSON 往返会重编码类型,仅纯数据可用 表 3-1
3 含特殊类型或循环引用用 structuredClone 表 3-2
4 需保留描述符与原型时手写递归 表 3-2

表 6-1 结论汇总

参考文献

[1] MDN Web Docs · structuredClone,https://developer.mozilla.org/zh-CN/docs/Web/API/structuredClone(访问于 2026-10-31)
[2] HTML Standard · Structured serialize,WHATWG,https://html.spec.whatwg.org/multipage/structured-data.html(访问于 2026-10-31)

返回文章列表

评论 0

0/2000

还没有评论,快来抢沙发吧~