core-js 中的 structuredClone:HTML 标准结构化克隆的 Polyfill 原理与实战
【免费下载链接】core-jsStandard Library项目地址: https://gitcode.com/GitHub_Trending/co/core-js
structuredClone是 WHATWG HTML 标准提供的全局深拷贝 API,能够递归克隆绝大多数 JavaScript 内置类型与平台对象,并天然支持循环引用、共享引用与ArrayBuffer转移。本文以 core-js 对web.structured-clone模块的完整实现为主线,讲解该 polyfill 的入口点、可克隆类型清单、源码级克隆算法,以及transfer选项的引擎边界与替代方案,帮助你在旧浏览器、旧版 Node.js 等环境中安全落地深拷贝逻辑。
从 JSON 深拷贝到结构化克隆
在structuredClone出现之前,JavaScript 社区做"深拷贝"最常用的手段是JSON.parse(JSON.stringify(obj)),但它存在大量硬伤:
- 遇到
undefined、函数、Symbol属性时会被静默丢弃; Date、RegExp、Map、Set、ArrayBuffer、TypedArray等类型会被错误地序列化成普通对象或空对象;- 循环引用直接抛出
TypeError: Converting circular structure to JSON; - 无法保留对象的原型与共享引用关系。
而 HTML 标准的structuredClone正是为补足这些能力而设计的:它实现了 HTML 标准中的结构化克隆算法 一节的原始说明。
模块与入口点
structuredClone的 polyfill 实现位于 modules/web.structured-clone.js,其挂载方式是向全局对象注入一个可枚举的structuredClone方法:
// https://html.spec.whatwg.org/multipage/structured-data.html#dom-structuredclone $({ global: true, enumerable: true, sham: !PROPER_STRUCTURED_CLONE_TRANSFER, forced: FORCED_REPLACEMENT }, { structuredClone: function structuredClone(value /* , { transfer } */) { /* ... */ } });forced: FORCED_REPLACEMENT意味着当检测到引擎自带的structuredClone实现存在语义缺陷时会强制替换为 core-js 版本(详见后文"引擎差异"小节)。sham: true表明在无法实现真正的 transferable 语义时,该 polyfill 被标记为"sham"(不完整的替代实现)。
按文档说明,structuredClone对应唯一的模块名web.structured-clone,其标准入口点为:
core-js(-pure)/stable|actual|full/structured-clone即四个命名空间(stable、actual、full,以及纯版本core-js-pure)下都提供了同名入口文件,仓库中对应的实际文件为:
- stable/structured-clone.js:仅稳定 ES 特性 + Web 标准,会额外预载
DOMException构造器相关模块; - actual/structured-clone.js:基于
stable,另含 stage 3 提案; - full/structured-clone.js:基于
actual,另含早期提案; - web/structured-clone.js:仅包含 Web 标准类别的最小依赖集合。
具体用法(完整入口点体系见 docs/web/docs/usage.md):
// 全局版本:直接注入全局 structuredClone import 'core-js/stable/structured-clone'; // 或一次性引入全部稳定特性 import 'core-js/stable'; // 纯版本:不污染全局命名空间,返回函数 import structuredClone from 'core-js-pure/stable/structured-clone'; // 也可以直接按需使用 import structuredClone from 'core-js-pure/actual/structured-clone';由于structuredClone是 Web 标准方法而非 ES 方法,在core-js-pure纯版本中不会挂载到全局对象上,而是作为导出的函数使用,避免任何命名空间污染。
函数签名与参数说明
官方文档给出的类型签名为:
function structuredClone(value: Serializable, { transfer?: Sequence<Transferable> }): any;| 参数 | 类型 | 说明 |
|---|---|---|
value | Serializable | 要深拷贝的源值。原始类型直接原样返回;可序列化对象递归克隆 |
options.transfer(可选) | Sequence<Transferable> | 可转移对象列表(如ArrayBuffer、MessagePort、ImageBitmap等)。转移成功后原对象会被 detach(剥离/清空),数据所有权移交克隆体 |
从源码看,第二个参数的处理非常严谨(modules/web.structured-clone.js):
var options = validateArgumentsLength(arguments.length, 1) > 1 && !isNullOrUndefined(arguments[1]) ? anObject(arguments[1]) : undefined; var transfer = options ? options.transfer : undefined;- 少于 1 个参数时直接抛错(测试断言
structuredClone()必须抛出); - 第二参数为
null或undefined时视作未传,正常运行(structuredClone(1, null)返回1); - 传入了对象但没有
transfer属性时,只做普通克隆,不做转移。
可克隆类型全览
structuredClone能正确处理以下类型(对应文档示例,均可直接运行):
structuredClone(42); // => 42 structuredClone({ x: 42 }); // => { x: 42 } structuredClone([1, 2, 3]); // => [1, 2, 3] structuredClone(new Set([1, 2, 3])); // => Set{ 1, 2, 3 } structuredClone(new Map([['a', 1], ['b', 2]])); // => Map{ a: 1, b: 2 } structuredClone(new Int8Array([1, 2, 3])); // => new Int8Array([1, 2, 3]) structuredClone(new AggregateError([1, 2, 3], 'message')); // => new AggregateError([1, 2, 3], 'message') structuredClone(new TypeError('message', { cause: 42 })); // => new TypeError('message', { cause: 42 }) structuredClone(new DOMException('message', 'DataCloneError')); // => new DOMException('message', 'DataCloneError') structuredClone(document.getElementById('myfileinput')); // => new FileList structuredClone(new Date('1970-01-01')); // => Date "Thu Jan 01 1970 00:00:00..." structuredClone(new Blob(['test'])); // => new Blob(['test']) structuredClone(new ImageData(8, 8)); // => new ImageData(8, 8) // etc.不可序列化类型(函数、Symbol、全局对象、Event、MessagePort等)会抛出DataCloneError:
structuredClone(new WeakMap()); // => DataCloneError on non-serializable types循环引用与共享引用
这是structuredClone相对 JSON 方案的杀手级能力——克隆结果保留引用结构而非值快照:
const structured = [{ a: 42 }]; const sclone = structuredClone(structured); console.log(sclone); // => [{ a: 42 }] console.log(structured !== sclone); // => true(顶层引用不同) console.log(structured[0] !== sclone[0]); // => true(嵌套对象也完全独立) const circular = {}; circular.circular = circular; const cclone = structuredClone(circular); console.log(cclone.circular === cclone); // => true(循环引用被正确保留,指向克隆体自身)实现层面,这个能力来自克隆算法内部的Map记忆表(见下文源码分析),structuredCloneInternal每克隆一个对象都会登记到map中,再次遇到同一引用时直接返回已克隆对象,从而同时解决循环引用与共享引用去重两个问题。在 tests/unit-global/web.structured-clone.js 中专门验证了这一语义:{ a: shared, b: shared }克隆后multiClone.a === multiClone.b,证明共享引用没有产生两份拷贝。
源码级实现原理
polyfill 的核心是structuredCloneInternal(value, map)这个递归克隆函数(modules/web.structured-clone.js),其执行流程可以概括为:
- 原始值短路:
Symbol直接抛Uncloneable type: Symbol;非对象值(!isObject(value))原样返回; - 记忆表查重:如果
map中已有该引用,直接返回已有克隆,实现循环引用与共享引用保真; - 按
classof分派:使用 core-js 内部的classof工具获取对象的内置标签(如Array、Map、Set、RegExp、Error、DOMException、ArrayBuffer、各类 TypedArray、Date、Blob、File、ImageData、几何类型等),为每类类型走专属克隆分支; - 登记克隆体:将
value -> cloned写入map,随后再递归填充内容(属性、Map 键值、Set 元素、错误 message/cause/stack 等)。
几个值得注意的实现细节:
- RegExp:不直接依赖引擎的 RegExp 构造克隆,而是用
value.source与getRegExpFlags(value)重新构造,以规避 Safari 14.1 无法克隆部分 flags 的 bug; - ArrayBuffer 家族:
cloneBuffer优先调用value.slice(0),若缓冲区可伸缩(resizable)则改用new ArrayBuffer(length, { maxByteLength })并逐字节复制;SharedArrayBuffer由于共享内存语义无法 polyfill,在无原生支持时直接返回原对象(源码注释明确说明"we can't polyfill it, so return the original"); - Error 类:按
name分派到AggregateError、内置错误、WebAssembly.CompileError/LinkError/RuntimeError等分支,并额外复制message、cause、errors、suppressed与可安装的stack属性; - FileList:优先通过
DataTransfer的items.add()逐个重建文件列表,DataTransfer不可用时回退到受限的原生克隆; - 兜底策略:对于
AudioData、VideoFrame等平台类型,要求其具备clone()方法;对于CryptoKey、ImageBitmap、WebAssembly.Module等无法同步克隆的类型,直接抛出DataCloneError("cannot be properly polyfilled in this engine")。
transfer 选项:能力、边界与官方警告
transfer选项允许在克隆的同时转移可转移对象的所有权——原对象被 detach(如ArrayBuffer的byteLength变为 0),克隆体接管底层内存,从而避免大块数据的复制开销:
const buffer = new ArrayBuffer(8); const view = new Uint8Array(buffer); view.set([1, 2, 3, 4]); const clone = structuredClone(buffer, { transfer: [buffer] }); // clone 为独立的 8 字节缓冲区 // 原 buffer.byteLength === 0(已被剥离)tryToTransfer会先校验 transfer 序列中每个元素都是对象,并拒绝重复的 transferable(抛Duplicate transferable)。对于ArrayBuffer采用"先登记、后剥离"的两阶段策略:先克隆所有缓冲,再通过detachBuffers统一剥离,原因在源码注释中说明:受"克隆已转移缓冲区的视图"问题影响,必须延后剥离(对应 core-js issue #1265)。
不过,官方文档对transfer给出了明确的警告:
[!WARNING]
- 许多平台类型在大多数引擎中都无法真正 transfer(polyfill 无法模拟该行为),但
.transfer选项对部分平台类型有效。推荐尽量避免使用该选项。- 部分特定平台类型在旧引擎中无法克隆。主要是一些非常特殊的类型或非常旧的引擎,但也存在例外。例如在 Safari 14.0- 或 Firefox 83- 中无法同步克隆
ImageBitmap,如需克隆特定类型,建议查看 polyfill 源码。
PROPER_STRUCTURED_CLONE_TRANSFER(internals/structured-clone-proper-transfer.js)用于探测引擎是否具备"正确的" transfer 语义:它创建一个 8 字节ArrayBuffer并尝试structuredClone(buffer, { transfer: [buffer] }),若原 buffer 未被剥离或克隆体长度不正确,则认为该引擎的 transfer 实现不合格。探测还对不同运行时做了 V8 版本门槛(Deno V8 > 92、Node V8 > 94、浏览器 V8 > 97),以规避 V8 的 ArrayBufferDetaching protector cell 失效导致的性能退化问题。
引擎差异与强制替换策略
core-js 之所以在原生已有structuredClone的引擎上仍然可能启用 polyfill,是因为历史上有大量引擎实现存在语义缺陷。源码中的检测逻辑(FORCED_REPLACEMENT)会逐一验证:
- 错误对象克隆:早期 FF 与 Safari 无法克隆 Error(如 FF
<103),FF103 克隆的.stack为空字符串,FF104 修复普通错误但DOMException仍有问题; - 错误引用去重:Chrome
<102在克隆对象包含多处同一错误引用时返回null(V8 issue 12542); - 新错误克隆语义:只有 FF103+ 支持 WHATWG html/5749 的新语义(
AggregateError的name、errors、cause均需正确克隆); - Node.js:Node 实现无法克隆
DOMException(nodejs/node#41038),Node<17.2的performance.mark克隆实现过于朴素,无法克隆RegExp或装箱原始值。
此外,模块还实现了一个巧妙的"备胎"方案:在完全没有原生structuredClone的引擎中,尝试用new PerformanceMark(uid, { detail: value }).detail从PerformanceMark的 detail 字段"借道"取回克隆值,用于checkBasicSemantic验证后的受限克隆路径。
这些引擎差异同样体现在tests/unit-global/web.structured-clone.js中:测试用例(源自 WPT 结构化克隆测试集)覆盖了原始值、装箱原始值、Date、RegExp、ArrayBuffer、可伸缩ArrayBuffer、全部 TypedArray、DataView、Map/Set、各类 Error、数组/对象、几何类型(DOMMatrix、DOMPoint、DOMQuad、DOMRect及其 ReadOnly 变体)、ImageData、Blob、File、FileList、循环/共享引用、transfer剥离以及不可序列化类型的异常路径。
实践建议与常见问题
- 优先用
actual命名空间按需引入:import 'core-js/actual/structured-clone'既能覆盖最新稳定 Web 标准,又不会带入早期不稳定提案; - 服务端场景:Node.js 16.0+ 才有
structuredClone,且 Node<17.2的实现有缺陷——core-js 会通过FORCED_REPLACEMENT自动替换,因此低版本 Node 直接引入 core-js 即可获得符合规范的行为; - 深拷贝大对象:普通克隆走递归复制;只有当数据体积大、且目标对象确属可 transfer 类型(如
ArrayBuffer)时才考虑transfer,并接受"部分平台类型无法 polyfill"的边界; - 克隆失败统一为
DataCloneError:不可克隆类型(函数、Symbol、WeakMap、全局对象等)一律抛出DOMException(name 为DataCloneError),可据此做统一的 try/catch 降级处理。
小结
structuredClone补全了 JavaScript 深拷贝在类型覆盖、循环引用、共享引用与内存转移四个维度的能力,而 core-js 的 web.structured-clone 模块则在其上提供了跨引擎一致的实现:既有完善的类型分派与引用保真算法,也有针对各引擎历史 bug 的探测与强制替换机制。无论你是在兼容旧浏览器,还是在为低版本 Node.js 补齐全局 API,按core-js(-pure)/stable|actual|full/structured-clone入口点引入即可获得与规范对齐的深拷贝能力。
【免费下载链接】core-jsStandard Library项目地址: https://gitcode.com/GitHub_Trending/co/core-js
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考