js 内置数据深拷贝 API-structuredClone()

介绍

全局的 structuredClone() 方法使用结构化克隆算法(见下文)将给定的值进行深拷贝

该方法还支持把原始值中的可转移对象转移到新对象,而不是把属性引用拷贝过去,支持循环引用。 可转移对象与原始对象分离并附加到新对象;它们不可以在原始对象中访问被访问到。

语法

1
2
structuredClone(value)
structuredClone(value, { transfer })

其中参数:

  • value:被克隆的对象。可以是任何结构化克隆支持的类型。
  • transfer: 可选。是一个可转移对象的数组,里面的 并没有被克隆,而是被转移到被拷贝对象上。

返回值:

示例:

1
2
3
4
5
6
7
8
9
10
// Create an object with a value and a circular reference to itself.
const original = { name: 'MDN' };
original.itself = original;

// Clone it
const clone = structuredClone(original);

console.assert(clone !== original); // the objects are not the same (not same identity)
console.assert(clone.name === 'MDN'); // they do have the same values
console.assert(clone.itself === clone); // and the circular reference is preserved

structuredClone支持拷贝 js 的各种内置的类型,比如:DateSetMapErrorRegExpArrayBufferBlobFileImageData

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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

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 DOMPoint(1, 2, 3, 4)); // => new DOMPoint(1, 2, 3, 4)
structuredClone(new Blob(['test'])); // => new Blob(['test'])
structuredClone(new ImageData(8, 8)); // => new ImageData(8, 8)
// etc.

API 规范

https://html.spec.whatwg.org/multipage/structured-data.html#transferable-objects

p-3

结构化克隆算法(Structured clone algorithm)

结构化克隆算法是 ECMAScript 2019 中引入的。 它使用了一种新的算法,该算法可以复制任意类型的值,包括循环引用。结构化克隆算法是用于复制复杂 js 对象的算法。

结构化克隆所不能做到的

  • Function 对象是不能被结构化克隆算法复制的;如果你尝试这样子去做,这会导致抛出 DATA_CLONE_ERR 的异常。
  • 企图去克隆 DOM 节点同样会抛出 DATA_CLONE_ERR 异常。
  • 对象的某些特定参数也不会被保留
    • RegExp 对象的 lastIndex 字段不会被保留
    • 属性描述符,setters 以及 getters(以及其他类似元数据的功能)同样不会被复制。例如,如果一个对象用属性描述符标记为 read-only,它将会被复制为 read-write,因为这是默认的情况下。
    • 原形链上的属性也不会被追踪以及复制。

支持的类型

JavaScript 类型:ArrayArrayBufferBooleanDataViewDateError 类型(仅限部分 Error 类型)。MapObject 对象:仅限简单对象(如使用对象字面量创建的)。除 symbol 以外的基本类型。RegExplastIndex 字段不会被保留。SetStringTypedArray

其中 Error 类型仅支持以下:ErrorEvalErrorRangeErrorReferenceErrorSyntaxErrorTypeErrorURIError(或其他会被设置为 Error 的)。

浏览器必须序列化 namemessage 字段,其他有意义的字段则可能会序列化,如 stackcause 等。

兼容性

p-1
p-2

注意:Nodejs 是 v17.0 起支持。

Structured Clone 算法其实早已在部分浏览器场景中出现。例如,每当调用postMessage将消息发送到另一个窗口或 WebWorker 时、或者使用IndexedDB 中存储 JS 值时,都会使用到这个算法。

Polyfill

实现大致可以概括为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 以下是core-js对于structuredClone实现的伪代码

function structuredClone(value, transfers) {
// 1. 检查浏览器原生实现是否正确
if (原生实现正确) {
return 原生structuredClone(value, transfers);
}

// 2. 创建 map 记录映射关系
map = new Map();

// 3. 实现深拷贝
function deepClone(value) {
if (map 中存在 value) {
return map中获取的克隆对象;
}

// 基于类型,拷贝对象
if (Array) {
克隆数组;
} else if (Object) {
克隆对象属性;
} else if (ArrayBuffer) {
克隆ArrayBuffer;
}
...

// 存储映射关系
map.set(value, cloneValue)

// 递归处理子属性
遍历属性深度拷贝属性值;

return cloneValue;
}

// 4. 处理 transfers
if (transfers) {
对transfers中对象执行转移操作;
}

// 5. 清理工作并返回
return deepClone(value);
}

主要利用深度优先递归遍历的方式,实现对对象及其属性的克隆。并使用 Map 记录映射关系,针对不同类型做定制化处理。

主要功能点如下:

  • 检查浏览器原生的 structuredClone 实现是否正确。包括: 克隆 Set、错误对象、新错误对象语义等的支持。
  • 如果原生实现有问题,则使用自己的 polyfill 实现代替。
  • polyfill 通过递归遍历对象,根据类型创建副本。支持各种基础类型、ArrayBuffer、错误对象等的克隆。
  • 支持 transferables 参数,用于转移对象的所有权,避免拷贝。调用原生的结构化克隆方法实现。
  • 处理各种边界案例:重复 transferables、不可 transfer 的类型、分离 ArrayBuffers 等。
  • 最终返回一个深层次拷贝的 clone 对象。

所以这是一份功能完整的 structuredClone polyfill。它考虑了规范要求、不同浏览器的实现问题、边界案例等,提供了强大的备选实现。

core-js 中的 structuredClone 的 polyfill ,还没有解决 ArrayBuffer 实例和许多平台类型无法在大多数引擎中传输的问题,所以 当需求兼容较低版本浏览器是因尽量避免使用 structuredClone(value, { transfer }) 的第二个参数。

另外还有份单独的 polyfill 也可以参考:https://github.com/ungap/structured-clone

对比其他深拷贝方法

日常开发中,我们经常使用 JSON.parse(JSON.stringify(obj)) 或者 lodash 中的_.cloneDeep() 来实现深拷贝。

在不考虑大批量数据处理的性能情况下,单从日常使用以下做个简单的比较:

API JSON.parse(JSON.stringify()) _.cloneDeep() structuredClone()
优点 全局 api, 简单易用 支持复杂数据类型的拷贝 全局 api,简单易用;支持复杂数据类型的拷贝
缺点 只能处理基本对象、数组和原始类型。任何其他类型都会以难以预测的方式处理。例如,Date 被转换为字符串 这个函数会占用17.4 kb (5.3 kb gzip) 可能有兼容性问题(低端浏览器、低版本 nodejs)

总结

综合来看,structuredClone()可以作为一个深拷贝的优选方案,在使用中需要考虑一下兼容性(polyfill 的适配)情况。


参考资料