告别老套!structuredClone:新一代深拷贝神器,让JSON.parse和JSON.stringify靠边站!

1,423 阅读7分钟

前言

Hello,大家好,我是秋天的一阵风~

最近项目正处于上线阶段,工作量相对较少,所以我终于有了一些空闲时间来回顾一下项目中的老代码。在这次代码审查过程中,我发现我们项目中对对象进行深拷贝的频率相当高,而且我们使用的还是最简单、最常见的JSON.parse和JSON.stringify方法。请看下图:

image.png

谈到深拷贝,这是个老生常谈的话题,并且在前端的笔试和面试中出现的频率极高。我们在项目中往往会 “贪图” 方便和快捷,选择JSON.parseJSON.stringify方法来实现深拷贝。对于如stringbooleannumber等一些简单的基本数据类型,这种方法确实无可厚非。然而,如果数据中包含了复杂的数据类型,这种方法就会引发各种问题。下面我们将一一展开讨论,揭示其中的坑点。

一、存在的问题:复杂数据类型拷贝会出错

我们直接上代码看结论:

      /**
        * 简单深拷贝的缺点:JSON.parse(JSON.stringify())
        */
       let simpleDeepClone = {
         a: 1,
         b: { name: "owllai" },
         c: null, // null
         d: undefined, //丢失
         f: new Date(), // 变成时间字符串
         g: new RegExp(/\d/), //
         h: new Error(),    // new RegExp 和 new Error 变成空对象
         i: NaN,
         j: Infinity, //NAN 和 Infinity 会变成null
         k: new Map(), //变成空对象
         l: new Set(), //变成空对象
       };
       console.log("简单深拷贝的缺点");
       const cloneObj = JSON.parse(JSON.stringify(simpleDeepClone))
       console.log(cloneObj);

image.png

  1. undefined 数据类型会丢失
  2. Date 时间类型会变成时间字符串
  3. 正则类型RegExpError 错误类型 会变成空对象
  4. NANInfinity 会变成 null
  5. MapSet 会变成空对象

可以看到,如果数据含有复杂数据类型时,使用JSON.parseJSON.stringify进行的深拷贝问题还是不少的,除此之外还有引用循环的问题

二、存在的问题:循环引用

循环引用指的是在对象结构中,某个对象直接或间接地引用了自身,形成了一个闭环。

当你尝试使用上述方法深拷贝一个有循环引用的对象时,JSON.stringify()会抛出一个错误,因为它无法序列化这样的对象结构。

以下是一个简单的例子来说明这个问题:

let obj = {
  name: 'Alice',
  friends: []
};

// 引入循环引用
obj.self = obj;
obj.friends.push(obj);

// 尝试深拷贝
try {
  let cloneObj = JSON.parse(JSON.stringify(obj));
} catch (error) {
  console.error('Error:', error.message);
}

在这个例子中,obj对象有一个name属性,一个friends数组,以及一个self属性,self属性指向obj本身,造成了循环引用。friends数组也包含了obj,进一步增加了循环的复杂性。

当你运行这段代码时,JSON.stringify(obj)会抛出一个错误,类似于Converting circular structure to JSON,这是因为JSON.stringify()检测到了循环引用,并且它不知道如何处理这种结构,因此终止执行并抛出错误。

image.png

三、请出本篇的主角:structuredClone

structuredClone 是一项强大的 Web API,允许开发者深拷贝对象,包括那些 JSON 序列化不支持的复杂数据结构。这个 API 是作为一个独立的全局方法在现代浏览器中实现的,旨在提供一种安全且高效的方式来复制对象。

1. structuredClone API 的特点

  1. 支持多种类型: structuredClone 不仅支持原始数据类型,还支持如 DateMapSetArrayBufferImageData 等复杂类型,甚至可以复制错误对象和正则表达式。
  2. 处理循环引用: 与 JSON 方法不同,structuredClone 可以正确处理对象之间的循环引用,这使得它在处理复杂数据结构时不会出错。
  3. 传输转移对象: 该 API 支持“transferable objects”,例如 ArrayBuffer,这意味着它可以在不复制实际数据的情况下将内存从一个上下文转移到另一个上下文,从而提高性能。
  4. 安全性: structuredClone 由浏览器内部实现,避免了执行不安全代码的风险,比如当使用 eval() 来解析和重新创建对象时可能遇到的问题。

2. 使用场景

  • Web Workers: 在主线程和 Web Worker 之间传递复杂数据时,structuredClone 提供了一种安全且高效的方法。
  • IndexedDB: 在存储复杂结构数据到 IndexedDB 时,可以用 structuredClone 来确保数据的完整性。
  • 消息传递: 使用 postMessage 方法在不同的浏览器上下文之间发送数据时,structuredClone 能够确保数据的结构和类型被完整保留。

3. 语法

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

参数

返回值

返回值是原始深拷贝

4. 使用案例

我们用之前的例子试试这个新API:structuredClone的效果

复杂数据类型

          let simpleDeepClone = {
               a: 1,
               b: { name: "owllai" },
               c: null, // null
               d: undefined, //丢失
               f: new Date(), // 变成时间字符串
               g: new RegExp(/\d/), //
               h: new Error(),    // new RegExp 和 new Error 变成空对象
               i: NaN,
               j: Infinity, //NAN 和 Infinity 会变成null
               k: new Map(), //变成空对象
               l: new Set(), //变成空对象
             };
             const structuredCloneObj = structuredClone(simpleDeepClone)
             console.log(structuredCloneObj);

image.png

循环引用

let obj = {
  name: 'Alice',
  friends: []
};

// 引入循环引用
obj.self = obj;
obj.friends.push(obj);

// 尝试深拷贝
try {
  let cloneObj = structuredClone(obj));
  console.log(cloneObj)
} catch (error) {
  console.error('Error:', error.message);
}

image.png

可选参数 transfer

structuredClone 除了接收被克隆对象以外,还有一个可选参数transfer

structuredClone 方法中的 transfer 参数用于优化性能和内存管理,尤其是在处理 Blob 或者 ArrayBuffer 类型的数据时。当 transfer 参数被指定时,它允许原始数据在克隆过程中被移动到新的对象中,而不仅仅是复制。这意味着原始数据不再有效,从而避免了不必要的内存占用。

下面是一个使用 structuredClone 和 transfer 参数的例子:

const originalArrayBuffer = new ArrayBuffer(1024 * 1024); // 创建一个1MB的ArrayBuffer
const originalView = new Uint8Array(originalArrayBuffer);

// 填充数据
for (let i = 0; i < originalView.length; i++) {
  originalView[i] = i % 256;
}

// 使用 structuredClone 进行深拷贝,同时传递 ArrayBuffer
const clonedView = structuredClone(originalView, { transfer: [originalArrayBuffer] });

// 此时 originalArrayBuffer 不再有效,因为其数据已经被移动到克隆对象中
console.log(clonedView); // 输出克隆后的视图
console.log(originalView); // 输出可能为空或未定义,具体取决于浏览器实现

在这个例子中,我们创建了一个 ArrayBuffer 并用 Uint8Array 视图填充了一些数据。然后我们调用 structuredClone 方法,并通过 transfer 参数传递了 originalArrayBuffer。这表示我们将把原始缓冲区的数据移动到克隆的对象中,而原始的 ArrayBuffer 将会被释放,以节省内存。

需要注意的是,transfer 参数只能接受那些可以被移动的类型,如 ArrayBuffer 或 Blob。如果尝试传递其他类型的对象,structuredClone 将会忽略 transfer 参数并正常克隆对象。此外,transfer 参数必须是一个数组,即使只转移一个对象。

在实际应用中,transfer 参数特别适用于处理大量二进制数据的场景,如 Web Workers 或者跨窗口传输数据,因为它可以显著减少内存消耗和提高性能。

四、不足之处

1. 不支持非结构化克隆支持的类型

足够细心的同学应该发现我们上面的例子中其实缺了好几种数据类型的测试,比如symbolfunction类型,我们现在加上发现控制台其实会报错:

  let simpleDeepClone = {
                 a: 1,
                 b: { name: "owllai" },
                 c: null, // null
                 d: undefined, //丢失
           +     e:function(){},
                 f: new Date(), // 变成时间字符串
                 g: new RegExp(/\d/), //
                 h: new Error(),    // new RegExp 和 new Error 变成空对象
                 i: NaN,
                 j: Infinity, //NAN 和 Infinity 会变成null
                 k: new Map(), //变成空对象
                 l: new Set(), //变成空对象
           +     m: Symbol("1")
               };
               console.log("简单深拷贝的缺点");
               const cloneObj = JSON.parse(JSON.stringify(simpleDeepClone))
               console.log(cloneObj);
               const structuredCloneObj = structuredClone(simpleDeepClone)
               console.log(structuredCloneObj);

image.png

这是因为MDN中明确说明了structuredClone只能接收结构化克隆支持的类型,这个听起来感觉有点高大上,什么是结构化克隆支持的类型呢?我们来看下MDN的解释

image.png

可以看出mdn中明确表示了不支持symbol类型和Function类型,除此之外,DOM节点还有RegExp对象的特定参数也不会被保留。同学们在使用时需要特别注意这一点。

2. 兼容性

因为是2022年后才新添加的API,所以structuredClone在兼容性上的表现并不是非常好。

截至目前,structuredClone 在以下环境中受支持:

  • 桌面浏览器

    • Chrome 98及以上
    • Firefox 94及以上
    • Safari 15.4及以上
    • Edge 98及以上
  • 移动浏览器

    • Chrome for Android 98及以上
    • Firefox for Android 94及以上
    • Safari on iOS 15.4及以上
    • Samsung Internet 16.0及以上
image.png image.png