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+ 全局可用。它能处理循环引用、共享引用,也能搬 Date、Map、Set、RegExp、ArrayBuffer、TypedArray、Blob、File、BigInt。但对 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。