JS structuredClone实现数组深拷贝的方法与兼容性
详细介绍使用 JavaScript 原生 structuredClone API 实现复杂数组深拷贝的方法、底层机制、与传统浅拷贝及 JSON 序列化的关键差异、边界限制与全平台兼容性方案。
structuredClone() 是官方标准的解决方案。它基于 HTML 规范中的结构化克隆算法(Structured Clone Algorithm),能递归遍历多维嵌套数组并为每个引用对象分配独立的堆内存,彻底规避了展开运算符浅拷贝的共享引用隐患,同时也解决了 JSON.parse(JSON.stringify()) 丢失特定类型和遇到循环引用直接抛错的硬伤。
一、使用 structuredClone 进行数组深拷贝的具体方法
在支持该 API 的环境中,structuredClone 作为一个挂载在全局作用域(浏览器 window、Node.js global 以及 Web Worker self)上的原生函数,其基本调用格式非常直接:
// 基础调用语法
const clonedArray = structuredClone(originalArray);
为了理解为什么必须使用深拷贝,我们先来看日常开发中极易产生隐蔽 Bug 的“浅拷贝”行为。很多开发者习惯使用展开运算符 [...arr] 或 arr.slice() 来复制数组。但这些操作只复制了数组的最外层容器;如果数组元素中包含对象、子数组或其他引用数据类型,复制的只是其在内存中的引用指针。
// 场景对比:浅拷贝 vs structuredClone 深拷贝
const originalUsers = [
{ id: 1, name: "Alice", tags: ["frontend", "react"] },
{ id: 2, name: "Bob", tags: ["backend", "node"] }
];
// 1. 使用浅拷贝 (展开运算符)
const shallowUsers = [...originalUsers];
// 修改浅拷贝内部的嵌套对象
shallowUsers[0].name = "Alicia";
shallowUsers[0].tags.push("vue");
// 观察原数组:原数组被同步篡改!
console.log(originalUsers[0].name); // 输出: "Alicia" (数据污染)
console.log(originalUsers[0].tags); // 输出: ["frontend", "react", "vue"]
// 2. 使用 structuredClone 进行深拷贝
const deepUsers = structuredClone(originalUsers);
// 修改深拷贝数据
deepUsers[1].name = "Robert";
deepUsers[1].tags.push("rust");
// 观察原数组:原数组保持纯净,毫发无损
console.log(originalUsers[1].name); // 输出: "Bob" (完全隔离)
console.log(originalUsers[1].tags); // 输出: ["backend", "node"]
console.log(deepUsers[1].name); // 输出: "Robert"
深拷贝的执行过程不仅是新建一层外层数组,而是深度遍历原始数组中的每个索引项,当遇到普通对象、集合、数组时,在新的内存空间重新开辟存储块并赋值。
二、关键场景对比:structuredClone 与传统 JSON 方案的差异
在 structuredClone 出现之前,绝大多数教程和项目最普遍采用的“无依赖深拷贝”手段是 JSON.parse(JSON.stringify(arr))。这种方式利用 JSON 序列化与反序列化切断内存引用。然而,这种技巧并非真正的结构克隆,而是基于文本协议转换的折中方案。一旦数组中含有现代开发常用的复杂数据结构,就会出现严重的数据丢失甚至运行时崩溃。
1. 循环引用数组处理对比
在图形图谱、树形联动或具有父子反向引用的数组结构中,节点常常直接或间接包含对自身的引用。
// 构造一个包含循环引用的多维数组
const circularArray = [1, 2];
circularArray.push(circularArray); // 索引 2 引用自身
// 尝试使用 JSON 序列化
try {
JSON.parse(JSON.stringify(circularArray));
} catch (err) {
console.error(err.message);
// 输出: Converting circular structure to JSON
// 直接抛出 TypeError 异常导致程序中断
}
// 使用 structuredClone 进行深拷贝
const safeClonedArray = structuredClone(circularArray);
console.log(safeClonedArray[0]); // 输出: 1
console.log(safeClonedArray[2] === safeClonedArray); // 输出: true!完美复原循环拓扑
console.log(safeClonedArray[2] === circularArray); // 输出: false!具有全新的内存隔离
2. 特殊内置对象的保留对比
在现代数据处理中,数组往往承载着 Date、RegExp、Map、Set 或 Uint8Array 等数据对象:
const complexData = [
new Date("2026-10-31T08:00:00Z"),
/^[a-z]+$/gi,
new Set(["admin", "editor"]),
new Map([["status", 200]]),
undefined
];
// JSON 方案的问题
const jsonCloned = JSON.parse(JSON.stringify(complexData));
console.log(typeof jsonCloned[0]); // string! Date 对象被降级为普通字符串,原型方法丢失
console.log(jsonCloned[1]); // {}! 正则表达式变成空对象
console.log(jsonCloned[2]); // {}! Set 变成空对象,所有集合元素丢失
console.log(jsonCloned[3]); // {}! Map 变成空对象,键值全部蒸发
console.log(jsonCloned[4]); // null! undefined 被强制转换成了 null
// structuredClone 的表现
const nativeCloned = structuredClone(complexData);
console.log(nativeCloned[0] instanceof Date); // true,仍可调用 .getTime() 等全部原生方法
console.log(nativeCloned[1] instanceof RegExp); // true,保留正则规则与 flags: "gi"
console.log(nativeCloned[2] instanceof Set); // true,含有 "admin" 和 "editor"
console.log(nativeCloned[3] instanceof Map); // true,保留 status => 200 映射
console.log(nativeCloned[4]); // undefined,忠实保留原始值
3. 稀疏数组(Sparse Array)空位的保留
若数组中存在未赋值的空槽(Holes),例如通过 new Array(3) 或 [1, , 3] 声明的稀疏数组:
JSON.stringify会将空槽全部强行替换为null;structuredClone则会严格保持其稀疏性质,克隆后的数组仍然包含空槽,不会凭空产生null填充。
三、使用边界与 DataCloneError 异常处理
虽然 structuredClone 支持绝大多数结构化数据,但它严格遵循 HTML 规范定义的安全与可传输标准。如果试图拷贝以下类型的数据,引擎将立即抛出 DOMException: DataCloneError 异常:
structuredClone 将无法执行并抛出 DataCloneError:
- 函数与方法:普通的
function、箭头函数、类方法等; - DOM 节点:如
HTMLElement、Document节点; - 属性描述符与 getter/setter:只复制当前属性取值,
read-only或enumerable: false的配置会被擦除; - 原型链(Prototype):克隆由自定义类通过
new MyClass()实例化的对象时,克隆得到的新对象会丢失原有类的原型链,退化为普通纯Object。
针对此类边界,在面向用户输入或外部动态输入数组时,推荐采用防御性编程范式进行处理:
// 带有防御性回退的数组深拷贝封装示例
function safeDeepCloneArray(arr) {
if (!Array.isArray(arr)) {
throw new TypeError("参数必须为数组");
}
try {
// 优先尝试原生 structuredClone,性能好且全面支持 Map/Set/Date/循环引用
return structuredClone(arr);
} catch (error) {
if (error.name === "DataCloneError") {
console.warn("数组中包含不可克隆对象(如函数或DOM节点),退回手动浅拷贝或定制过滤方案");
// 场景化降级策略:例如过滤函数属性,或按需手动浅拷贝第一层
return arr.map(item => (typeof item === "object" && item !== null ? { ...item } : item));
}
throw error;
}
}
四、运行环境兼容性与降级方案
structuredClone 并非一个只属于浏览器的 DOM 扩展,它已被正式定义在 HTML 规范的顶层全局命名空间,并被 W3C 与各大主流引擎厂商收录在 Baseline 2022(全平台基线标准) 中。
1. 现代浏览器支持版本
- Google Chrome:98 及以上版本(2022 年 2 月发布)完全支持;
- Microsoft Edge:98 及以上版本同步支持;
- Mozilla Firefox:94 及以上版本(2021 年 11 月发布)率先实现;
- Apple Safari:15.4 及以上版本(macOS Monterey 及 iOS 15.4,2022 年 3 月)正式支持。
根据 Can I Use 统计,目前全球已有超过 96% 的活跃终端浏览器原生支持 structuredClone。在现代前端工程中,面向大众用户的项目通常可以直接放心使用。
2. 服务端与命令行运行时支持
- Node.js:在
17.0.0中首次引入实验支持,从 Node.js 18.0.0 LTS 开始作为稳定全局函数提供,无需任何额外require或import; - Deno:在
v1.14开始原生提供; - Bun:自
v0.1.0起全版本开箱即用支持。
3. 兼容旧环境的降级方案(Polyfill)
若业务场景需要继续支持较旧系统(例如 iOS 14、旧款内嵌 WebView 或 Node.js 16 以下),可以在工程入口引入成熟的 Polyfill 垫片。
// 方案 1: 安装轻量级 polyfill 包 (约 3.5KB,规范还原度高)
// npm install @ungap/structured-clone
import structuredClonePolyfill from '@ungap/structured-clone';
// 环境检测与平滑回退
const safeClone = typeof structuredClone === "function"
? structuredClone
: structuredClonePolyfill;
const myArray = [{ id: 1 }, new Set([1, 2, 3])];
const result = safeClone(myArray);
如果项目已经使用了 core-js 作为垫片集,也可以直接引入对应的子模块:
// 方案 2: 使用 core-js
import "core-js/actual/structured-clone";
五、总结与工程选型建议
在实现 JavaScript 数组深拷贝时,structuredClone 已经成为当之无愧的首选方案。在实际工程落地中,可以遵循以下三条建议:
- 简单扁平数组优先用浅拷贝:对于只包含数字、字符串等基本类型的纯一维数组,直接使用
[...arr]或arr.slice(),执行开销最低,性能表现最佳。 - 复杂结构数组默认选用 structuredClone:对于嵌套子对象、二维数组、带有时间/正则/集合或存在循环引用的复杂数组,统一采用
structuredClone(arr),避免自行编写递归函数带来的边界漏洞或JSON序列化的数据腐败。 - 警惕函数与 DOM 节点传递:在传递包含业务逻辑方法、观察者回调或页面元素的数组时,务必提前做好数据与视图/行为的解耦,防止抛出
DataCloneError。对于必须面向旧版本客户端的项目,提前配置@ungap/structured-clone垫片即可实现无缝降级。
JS语法错误会阻止加载吗?区分SyntaxError与资源加载失败
2026-11-05 16:08:46
JavaScript常见SyntaxError语法错误排查与修复方法
2026-11-05 13:58:44
JS中slice、substring和substr截取字符串的区别
2026-11-05 13:43:47
JS 转 TS 手动迁移最佳实践:从配置 tsconfig 到消除 any 类型
2026-11-05 11:53:32
为什么JS数组push后得到的是数字而不是数组?
2026-11-05 09:59:18
相关标签:






![JS创建二维数组:为什么Array(n).fill([])会导致数据联动](https://img.javascript.cn/tit/articles/13236/20261102/f618c991891ff37efb12df6af2ce0ccb0da8779a907fe1d2f7275d60c84fdd77.webp)

