侧边栏壁纸
博主头像
一笑痕

人生若只如初见,
是可喜亦或者是可悲?

  • 累计撰写 259 篇文章
  • 累计收到 7 条评论

structuredClone 数据全搬过来了,原型没搬

2026-9-15 / 0 评论 / 125 阅读

structuredClone 数据全搬过来了,原型没搬

前几年做深拷贝,标准答案基本是 JSON.parse(JSON.stringify(obj))。这招的问题在于它会偷偷改数据:Date 变成字符串,undefined 和函数直接消失,碰上循环引用还会报错。

后来 structuredClone 进了标准,Chrome 98 之后都能用。一开始以为终于有正经的深拷贝了,用了一阵子才发现它也有自己的脾气。每条一句结论,紧跟着的是脚本里的实际输出,不是从文档抄的。

能克隆的这一侧

嵌套对象和数组都是深拷贝,改副本动不到原对象

const src = { a: 1, b: { c: [1, 2, 3] } };
const cp = structuredClone(src);
cp.b.c.push(4);
// 原对象 b.c = [1,2,3] / 副本 b.c = [1,2,3,4]

循环引用也能克隆,JSON 方案在这直接报错

const cyc = { name: 'x' };
cyc.self = cyc;
const c = structuredClone(cyc);
c.self === c; // true

Map 和 Set 会变成空对象吗

类型 结果
Date instanceof Date 为 true,时间值一致
Map 还原成 Map,值是深拷贝
Set 还原成 Set
BigInt 还是 bigint
Uint8Array 还是 Uint8Array
TypeError 还原了,name 和 message 都在

Map 和 Set 这一条挺重要。JSON 方案会把它们变成空对象 {},而且不报错,等发现的时候通常已经在线上跑了。

前三条都属于日常最常见的数据形态,克隆结果和原件完全断开,改哪边都不影响另一边。剩下的十条讲的是会变形或者会抛错的地方。

会变形或者直接抛错的那几类

函数和 Symbol 直接抛异常,不返回 undefined

structuredClone({ fn: () => 1 });
// DOMException: () => 1 could not be cloned.

structuredClone({ s: Symbol('x') });
// DOMException: Symbol(x) could not be cloned.

structuredClone(new WeakMap());
// DOMException: #<WeakMap> could not be cloned.

structuredClone(new Proxy({ a: 1 }, {}));
// DOMException: #<Object> could not be cloned.

弱引用类型克隆不了是合理的,克隆出来的弱引用该指向谁呢。Proxy 克隆不了也好理解,代理的目标和陷阱函数没法序列化。

这些值都报同一种错,问题直接落在调用点上,比悄悄改写容易发现。脚本里每一项都套了 try/catch,抛出来的构造函数名和 message 原样打在控制台,文章里的报错原文就是那几行输出。

按 TypeError 分支去写,为什么抓不到

实测下来有个坑:在 Node 里写 catch (e) { if (e instanceof TypeError) ... } 是抓不到的,错误处理分支根本没进去。它抛的是 DOMException。

类实例的原型会丢,数据全在而方法没了

class User {
  constructor(n) { this.name = n; }
  greet() { return 'hi ' + this.name; }
}

const u = structuredClone(new User('金铭'));
u instanceof User;  // false
u.name;             // '金铭'  —— 数据在
u.greet;            // undefined —— 方法没了

数据全都复制过来了,但 prototype 没跟过来。规范里的说法是「结构化克隆」,不是「对象克隆」,它只处理数据,不处理行为。结果就是克隆出来的东西长得像 User,实际是个普通对象,调用它的方法时才报错,而且报错位置往往离克隆的地方很远。

对象上有方法就别用它,补原型只能救回一半

const u2 = structuredClone(plainUser);  // 先拿数据快照
Object.setPrototypeOf(u2, User.prototype);

类里有私有字段(#field)的话还是救不了,私有字段不在结构化克隆的范围内。

getter 会被求值,只读属性变成可写

const withGetter = {
  _x: 1,
  get x() { return this._x * 10; },
};
const g = structuredClone(withGetter);
g.x;  // 10
Object.getOwnPropertyDescriptor(g, 'x');
// { value: 10, writable: true, enumerable: true, configurable: true }

克隆读的是属性值,不是属性描述符。原本那个只读的计算属性,克隆完变成了一个可写的数据属性。代码如果依赖 getter 做校验或者惰性计算,克隆之后就失效了。

不可枚举的属性整个丢掉

const hidden = {};
Object.defineProperty(hidden, 'secret', { value: 42, enumerable: false });
structuredClone(hidden);  // {} —— secret 没了

冻过的对象,克隆出来完全可写

const f = structuredClone(Object.freeze({ a: 1 }));
Object.isFrozen(f);  // false

拿冻结对象当配置常量传给别处、指望它防篡改的话,克隆一次就白冻了。

稀疏数组的洞,变成真实存在的 undefined

const sp = structuredClone([1, , 3]);
1 in sp;   // true —— 原来是个洞,现在是个真实存在的 undefined

长度没错,但 in 判断和 Object.keys 的结果会变。

带 g 标志的正则,lastIndex 被重置

const re = /ab/g;
re.lastIndex = 5;
const c = structuredClone(re);
c.lastIndex;  // 0,不是 5

带 g 或 y 标志的正则用 exec 连续匹配时依赖 lastIndex。克隆一个匹配到一半的正则,进度就丢了,下次又从字符串开头开始找。这个坑不常见,真碰上了很难查。

纯数据用它,行为别指望它

日常的纯数据,比如接口返回的 JSON、配置对象、要存进 IndexedDB 的东西,structuredClone 比 JSON 方案好,直接用。对象上有方法、有 getter、有私有字段,或者依赖冻结状态,这一类我不推荐用:它给出来的是一份数据的快照,不是那个对象的副本。至于 JSON.parse(JSON.stringify()),现在没有理由再用它了:克隆不了的值它会静默改写,structuredClone 至少会大声报错。

    🤞 分享