← 回到手记
代码

深拷贝别再 JSON 来回倒了:用 structuredClone()

展开语法只拷一层,JSON.parse(JSON.stringify()) 会弄丢 Date、Set 和 undefined。浏览器和 Node.js 17+ 内置的 structuredClone() 能正确深拷贝,但它也有拷不了的东西。

要解决的问题

前端里「复制一份对象再改」是家常便饭:表单编辑时先拷一份草稿、撤销功能要存历史快照、Redux/状态管理里不想改到原数据。很多人第一反应是展开语法 { ...obj },或者老办法 JSON.parse(JSON.stringify(obj))。

前者只拷一层,后者会悄悄把数据改坏。现在浏览器和 Node.js 都内置了一个专门做深拷贝的全局函数:structuredClone()。MDN 标注它为 Baseline「广泛可用」,自 2022 年 3 月起主流浏览器都支持;Node.js 从 v17.0.0 起提供(见 Node.js 全局对象文档)。本文示例在本机 Node.js v20.19.2 实际跑过。

核心概念

先分清两个词:

  • 浅拷贝:只复制最外面一层,里面的数组、对象仍然和原对象共用同一份。MDN 在讲展开语法时说得很直白,大意是:展开语法实际上只深入一层,Object.assign() 也一样,JavaScript 没有原生操作能做深拷贝,需要深拷贝时可以用 Web API structuredClone()(见 Spread syntax)。
  • 深拷贝:把嵌套的东西也一路复制下去,新旧对象互不影响。

structuredClone(value) 用的是浏览器里的「结构化克隆算法」,postMessage() 给 Worker 传数据、IndexedDB 存对象,用的都是这套算法(见 The structured clone algorithm)。它支持的类型比 JSON 多得多:Date、Map、Set、RegExp、ArrayBuffer、各种 TypedArray、Blob、File 等,而且能正确处理循环引用。

示例代码

先看浅拷贝和 JSON 往返各自的坑:

// 1. 浅拷贝:嵌套对象还是同一份
const user = { name: "老伯", tags: ["python"], meta: { visits: 1 } };
const shallow = { ...user };
shallow.tags.push("js");
shallow.meta.visits = 99;
console.log(user.tags, user.meta.visits); // [ 'python', 'js' ] 99  原对象被改了

// 2. JSON 往返:类型丢失
const draft = {
  title: "草稿",
  savedAt: new Date("2026-10-25T04:00:00Z"),
  readers: new Set(["a", "b"]),
  extra: undefined,
};
const viaJson = JSON.parse(JSON.stringify(draft));
console.log(typeof viaJson.savedAt); // string   Date 变成了字符串
console.log(viaJson.readers);        // {}       Set 变成了空对象
console.log("extra" in viaJson);     // false    undefined 字段直接没了

这些都是 JSON.stringify 的既定行为:Date 有 toJSON(),会被序列化成 ISO 字符串;Map、Set 只看自身可枚举属性,所以变成 "{}";对象里的 undefined、函数、Symbol 会被省略(见 JSON.stringify())。

换成 structuredClone:

const copy = structuredClone(draft);
console.log(copy.savedAt instanceof Date); // true
console.log(copy.readers.has("b"));        // true
console.log("extra" in copy);              // true
console.log(copy !== draft, copy.readers !== draft.readers); // true true

// 循环引用也没问题
const node = { id: 1 };
node.self = node;
const c = structuredClone(node);
console.log(c.self === c); // true,而 JSON.stringify(node) 会直接抛 TypeError

它还有第二个参数 { transfer: [...] },可以把 ArrayBuffer 这类可转移对象「搬」到新对象里而不是复制,搬完原来的 buffer 就不能用了。日常业务用得少,处理大块二进制数据时再查 MDN 即可。

常见误区

  1. 以为什么都能克隆
    函数不能克隆,DOM 节点也不能,遇到会抛 DataCloneError:

js try { structuredClone({ onClick() {} }); } catch (e) { console.log(e.name); // DataCloneError }

所以别拿它去拷带回调的配置对象,或者 Vue/React 里带方法的复杂实例。

  1. 以为类实例克隆后还是那个类
    结构化克隆不会复制原型链,类实例拷出来就是普通对象,方法也没了:

js class Point { constructor(x, y) { this.x = x; this.y = y; } len() { return Math.hypot(this.x, this.y); } } const p = structuredClone(new Point(3, 4)); console.log(p instanceof Point, p.x, typeof p.len); // false 3 undefined

类的私有字段(#xxx)同样不会复制。需要保留类型时,自己写 clone() 方法。

  1. 以为 getter / 只读属性会保留
    MDN 明确说属性描述符、getter、setter 这类元信息不会复制。getter 会被求值成普通数据属性:{ get now() { return 42 } } 拷出来就是 { now: 42 },而且可写。另外 RegExp 的 lastIndex 也不会保留。

  2. 把它当成性能神器
    深拷贝本身就要遍历整个对象,数据很大时照样有开销。只需要改一层时,展开语法完全够用,也更快、更直观。别为了「保险」处处深拷贝。

  3. 忘了老环境
    老版本浏览器、Node.js 16 及以下没有这个全局函数。目标环境不确定时,先查 MDN 兼容性表,或者用 typeof structuredClone === "function" 判断。

小结

  • { ...obj } 和 Object.assign() 只拷一层,嵌套数据会共用。
  • JSON.parse(JSON.stringify(obj)) 会把 Date 变字符串、Map/Set 变空对象、丢掉 undefined,遇到循环引用直接报错。
  • 纯数据(普通对象、数组、Date、Map、Set 等)要深拷贝,就用 structuredClone()。
  • 它拷不了函数和 DOM 节点,不保留原型、getter 和私有字段;带行为的对象还是自己写拷贝逻辑。

一句话:拷数据用 structuredClone,拷「对象行为」别指望它。

参考资料

✳

谢谢你读到这里。