深拷贝别再 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 APIstructuredClone()(见 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 即可。
常见误区
- 以为什么都能克隆
函数不能克隆,DOM 节点也不能,遇到会抛DataCloneError:
js
try {
structuredClone({ onClick() {} });
} catch (e) {
console.log(e.name); // DataCloneError
}
所以别拿它去拷带回调的配置对象,或者 Vue/React 里带方法的复杂实例。
- 以为类实例克隆后还是那个类
结构化克隆不会复制原型链,类实例拷出来就是普通对象,方法也没了:
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() 方法。
-
以为 getter / 只读属性会保留
MDN 明确说属性描述符、getter、setter 这类元信息不会复制。getter 会被求值成普通数据属性:{ get now() { return 42 } }拷出来就是{ now: 42 },而且可写。另外RegExp的lastIndex也不会保留。 -
把它当成性能神器
深拷贝本身就要遍历整个对象,数据很大时照样有开销。只需要改一层时,展开语法完全够用,也更快、更直观。别为了「保险」处处深拷贝。 -
忘了老环境
老版本浏览器、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,拷「对象行为」别指望它。
参考资料
谢谢你读到这里。