编程 structuredClone 丢原型还不报错:Structured Clone 算法只复制数据,不复制行为

2026-09-08 00:05:54

structuredClone 丢原型还不报错:Structured Clone 算法只复制数据,不复制行为

审计快照链路里要把一条带 class 实例的规则对象 clone 一份再落库。图省事把项目里的 lodash.cloneDeep 换成了 structuredClone,落库前一切正常,下游拿快照调规则方法时直接 TypeError: snapshot.rule.magnitude is not a function

现场浓缩如下:

class MagnitudeRule {
  threshold = 3;
  magnitude() {
    return this.threshold;
  }
}

const clone = structuredClone(new MagnitudeRule());
clone.threshold;           // 3
clone.magnitude;           // undefined
Object.getPrototypeOf(clone) === MagnitudeRule.prototype; // false

底层是 HTML Structured Clone Algorithm,最初为 postMessage/IndexedDB 设计,2022 年起浏览器和 Node 17+ 全局可用。它能处理循环引用、共享引用,也能搬 DateMapSetRegExpArrayBufferTypedArrayBlobFileBigInt。但对 class 实例,算法只序列化自身可枚举数据字段,原型链不在协议里,方法挂在 prototype 上,所以 clone 出来是一张普通对象。克隆的是数据,不是行为。

第二个坑更直接:对象里带函数属性不是被丢弃,而是抛异常:

structuredClone({ callback: () => {} });
// DataCloneError: () => {} could not be cloned.

函数不是唯一拒绝类型。Vue reactive 返回的 Proxy、DOM 节点,一样走 DataCloneError。也就是说,如果 class 实例上挂的是箭头函数字段而不是 prototype 方法,事故会从「方法丢失」升级成「整个 clone 失败」。

还有一些不报错但会变形的边界:Symbol 键不会被遍历,不可枚举属性不会出现,getter/setter 不会以访问器形式被复制,结果里只留下被读取后的数据值。

transfer 是所有权转移,不是给深拷贝开加速

structuredClone 的第二参数可以转移可转移对象(如 ArrayBuffer):

const u8 = new Uint8Array([1, 2, 3]);
const cloned = structuredClone(u8, { transfer: [u8.buffer] });

u8.buffer.byteLength; // 0,原 buffer 被 detach

它适合「校验完、落库前把缓冲区所有权交出去」,避免后续代码再改原 buffer。做普通深拷贝时不要顺手带 transfer,否则会意外弄废原对象。

和 JSON 深拷贝的边界差异

JSON.parse(JSON.stringify(...)) 是更常见的方案,两者边界很不一样:

数据JSON 方案structuredClone
循环引用抛 TypeError正常
Date变字符串保留 Date
RegExp/Map/Set变空对象正常
NaN/Infinity变 null正常
BigInt抛 TypeError正常
函数属性静默丢弃抛 DataCloneError
Symbol 键静默丢弃同样丢弃

structuredClone 对结构化类型比 JSON 强很多,但函数和 Symbol 语义两边都覆盖不了。

替代方案怎么选

  • lodash.cloneDeep:各边界不抛错,更「贴心」,但函数仍是引用复制,Symbol 键会复制成新 Symbol,还要多引一个库。
  • MessageChannel + postMessage:底层是同一套算法,但要异步接收,属于 structuredClone 落地前的历史方案。
  • 手写递归:用 WeakMap 防循环引用,用 Object.create(Object.getPrototypeOf(...)) 重建原型,可定制程度高,但闭包函数和访问器语义仍然无法完美复刻。
  • 老 Node(<17)可以 require('v8').structuredClone,或者挂 core-js polyfill。

选择边界很清楚:纯 JSON 数据直接用 JSON.parse(JSON.stringify(...));出现 Date/Map/Set/循环引用时用 structuredClone;要处理函数字段或高度定制拷贝,才轮到手写递归或 lodash.cloneDeep

推荐文章

程序员茄子在线接单