通过学习🍊structuredClone🍊,学习深浅拷贝

前几天刷文章的时候,看到了一个用于深拷贝的API:structuredClone, 感觉很神奇很方便,以前写深拷贝的时候,不是自己利用递归写方法,就是使用第三方库的方法,例如:loadsh。

使用structuredClone这个api实现深拷贝很简单,而且提出时间也不短了,那为啥很少有人用啊,至少我见得少,也可能是我见识短浅了。那本文就先介绍structuredClone这个api,然后再讲述深拷贝和浅拷贝的知识。

structuredClone:🍨好用的深拷贝API🍨

我们先看下MDN官网: developer.mozilla.org/zh-CN/docs/...

怎么介绍structuredClone这个api的:

备注: 此特性在 Web Worker 中可用。
全局的 structuredClone() 方法使用结构化克隆算法将给定的值进行深拷贝
该方法还支持把原值中的可转移对象转移(而不是拷贝)到新对象上。可转移对象与原始对象分离并附加到新对象;它们将无法在原始对象中被访问。

这里我们可以看到 structuredClone() 方法有两个用途:

  • 数据深拷贝
  • 把原值中的可转移对象转移(而不是拷贝)到新对象上

那下面我们就介绍下它的这两种方法。

基本使用方法

js 复制代码
structuredClone(value)
structuredClone(value, { transfer })

参数

value:被克隆的对象。可以是任何结构化克隆支持的类型。

transfer (可选):是一个可转移对象的数组,里面的 值 并没有被克隆,而是被转移到被拷贝对象上。

返回值:是原始值的深拷贝。

异常 如果输入值的任一部分不可序列化,则抛出该异常。

用法还是挺简单的,没啥可说的。

参数里面的结构化支持的数据为:developer.mozilla.org/zh-CN/docs/...

下文会有总结structuredClone支持哪些数据。

structuredClone(value, { transfer }) 方法除了克隆对象外,还支持一个选项 { transfer },用于指定需要转移所有权的 transfer 数组。转移所有权意味着将某些对象的所有权从一个上下文(例如主线程)传递到另一个上下文(例如 Worker 线程),以提高性能和效率。

示例:

js 复制代码
// 在主线程中创建一个 ArrayBuffer
let buffer = new ArrayBuffer(16);
​
// 创建一个 Worker
let worker = new Worker('worker.js');
​
// 向 Worker 发送消息,并转移 ArrayBuffer 的所有权
worker.postMessage({ buffer }, [buffer]);
​
// 在 worker.js 中接收消息
self.onmessage = function(event) {
    let receivedBuffer = event.data.buffer;
    // 此时 receivedBuffer 可以在 Worker 中直接使用,不再属于主线程
};

在这个示例中,通过传递 { transfer: [buffer] },将 buffer 的所有权从主线程转移到了 Worker 线程,避免了数据的复制和额外的内存使用。

使用场景

常见的应用场景包括:

  • Web Workers:允许主线程和 Worker 线程之间传递复杂数据结构。
  • IndexedDB:在客户端存储中,使用 structuredClone 来存储和检索对象。
  • postMessage() :用于在不同的窗口或 iframe 之间安全地传递消息和数据。
  • BroadcastChannel:允许不同的文档或浏览器上下文之间通信,并通过 structuredClone 传输数据。

注意事项

虽然 structuredClone 提供了便利和安全性,但它也有一些限制:

  • 不能复制某些对象,如函数、 DOM 节点、Symbol、Error对象等。
  • 在不同浏览器或环境中,可能会有一些细微的实现差异或限制,需要注意兼容性问题

兼容性问题

深拷贝 :🍦彻底复制🍦

深拷贝(Deep Copy)是指在复制对象或数据结构时,不仅复制了对象本身,还递归复制了其所有内部嵌套的对象和数据。这确保了新创建的对象与原始对象完全独立 ,任何对复制对象的修改不会影响原始对象,反之亦然。

修改新对象不会影响原对象

简单理解就是复制一个和你相同但又相互独立的数据

实现深拷贝的方法,主要有以下四种:

  • 手动实现
  • JSON 序列化和反序列化
  • 使用第三方库
  • structuredClone

其实最经常使用的是前三种,第四种在上一章已经介绍过,主要用于Web Worker中,但是为啥这么简单,又很少用,这个问题还是百思不得其解。

我们主要介绍前三种方法。

手写递归方法

通过递归遍历对象的属性和数组元素,创建新的对象或数组,并复制每个属性的值或每个数组元素。这种方法需要处理循环引用和特殊类型(如日期、正则表达式)等情况。

js 复制代码
function deepCopy(obj, hash = new WeakMap()) {
  // 如果obj是原始类型,直接返回
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }
  
  // 如果obj已经在hash中,说明之前已经拷贝过,直接返回其拷贝的副本
  if (hash.has(obj)) {
    return hash.get(obj);
  }

  // 根据obj的类型创建一个新的实例
  let cloneObj;
  if (obj instanceof Array) {
    cloneObj = [];
  } else if (obj instanceof Date) {
    cloneObj = new Date(obj);
  } else if (obj instanceof RegExp) {
    cloneObj = new RegExp(obj);
  } else {
    cloneObj = new obj.constructor;
  }

  // 将新对象存入hash,避免无限递归
  hash.set(obj, cloneObj);

  // 递归处理obj的所有属性
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      cloneObj[key] = deepCopy(obj[key], hash);
    }
  }

  return cloneObj;
}

这个函数会遍历对象的所有属性,并对每个属性进行递归深拷贝。如果属性是对象,它会调用自身进行深拷贝。注意,这个函数不处理函数和Symbol类型的属性,也不处理循环引用的对象。对于更复杂的情况,可以使用第三方库如lodash_.cloneDeep()方法。

JSON.parse(JSON.stringify())

在 JavaScript 中,你可以利用 JSON.parse(JSON.stringify()) 来实现一个简单的深拷贝。这种方法适用于大多数情况。

以下是如何使用它的示例:

js 复制代码
const original = {
    a: [1, 2, 3],
    b: { inner: 'value' },
    c: [4, 5],
    d: { nested: { deeper: 'deep value' } },
    e: 'immutable string'
};

const copy = JSON.parse(JSON.stringify(original));

console.log(copy);

这个方法的优点是简单易用,并且对于大多数基本的嵌套对象和数组结构来说是有效的。

🦀但有一些限制和注意事项需要考虑🦀

  • 支持的数据类型:原始类型(如字符串、数字、布尔值)、普通对象、数组以及支持序列化的特殊对象(如日期对象),都可以被深拷贝。
  • 不支持的数据类型:JSON.stringify() 方法无法处理函数、正则表达式、undefined 等特殊数据类型,因此对于这些类型的对象,JSON.parse(JSON.stringify()) 并不适用。

🦞注意事项和限制🦞

  • 循环引用问题:JSON.stringify() 不支持处理循环引用,会报错 TypeError: Converting circular structure to JSON。这是因为 JSON 格式要求对象之间的引用是非循环的,无法处理形成闭环的情况。
  • 特殊对象类型:如函数、正则表达式等特殊对象类型,在转换为 JSON 字符串时会丢失其特殊属性和行为,因此无法通过 JSON.parse(JSON.stringify()) 来实现深拷贝。
  • 对于大型对象或嵌套层次很深的对象,JSON 序列化和反序列化可能会导致性能问题,因为这是一种比较消耗 CPU 资源的操作。

第三方库

使用第三方库实现深拷贝是一个灵活且常见的做法,特别是当需要处理复杂对象、特殊对象类型(如日期、正则表达式)、循环引用等情况时。

但是在使用的时候,注意要按需加载,避免不必要的内存损失。

以下是使用 Lodash 库中的 cloneDeep 方法来实现深拷贝的示例:

js 复制代码
import cloneDeep from 'lodash/cloneDeep';

var obj = { a: 1, b: { c: 2 } };
var copiedObj = cloneDeep(obj);

console.log(copiedObj); // { a: 1, b: { c: 2 } }
  • 按需加载:通过导入 lodash/cloneDeep,可以减少整体包的大小,只导入所需的功能,避免不必要的代码体积增大。

总结对比

上面介绍的实现深拷贝的四种方法,其中手写和第三方库都是可以不断完善优化的,但是JSON.parse(JSON.stringify())structuredClone是存在本身的限制的,并不是能深拷贝任何的数据类型,这里我们引用Sunshine_Lin 大佬的数据,说明JSON.parse(JSON.stringify())structuredClone支持的数据类型的情况。

JSON.parse(JSON.stringify()) structuredClone
number
string
undefined
null
boolean
object
Array
Function
map
Set
Date
Error
Regex
Dom节点

浅拷贝🍧表面复制🍧

浅拷贝(Shallow Copy)是指在复制对象或数据结构时,只复制对象本身及其第一层属性,而不会递归复制其内部嵌套的对象或数组。

这意味着新创建的对象与原始对象共享相同的内部对象引用,修改其中一个对象的内部对象会影响另一个对象。

注意点:浅拷贝是指复制对象的第一层属性,对于对象内部的引用类型(如数组、对象),浅拷贝后的新对象仍然会共享相同的引用。这意味着,通过浅拷贝得到的对象,其引用类型的属性仍然指向原始对象中相同的引用。因此,如果你对浅拷贝后的对象的引用类型属性进行直接赋值,会改变这些属性的引用,使得浅拷贝后的对象与原始对象不再共享相同的引用。

实现浅拷贝的方法有:

  • 使用拓展运算符 { ...obj }
  • 使用 Object.assign()
  • 使用 Array.prototype.slice()(仅适用于数组)

拓展运算符

使用拓展运算符 ... 可以非常方便地实现对象和数组的浅拷贝。

使用拓展运算符可以实现浅拷贝,它可以将一个数组或对象拆分为单独的元素,然后将这些元素复制到一个新的数组或对象中。

具体实现如下:

javascript 复制代码
const originalArray = [1, 2, 3, 4, 5];
const shallowCopy = [...originalArray];

console.log(shallowCopy); // [1, 2, 3, 4, 5]

在上面的代码中,我们使用拓展运算符将originalArray数组中的元素拆分为单独的元素,并将它们复制到shallowCopy数组中,从而实现了浅拷贝。

🦑需要注意的是🦑,拓展运算符只能用于可迭代对象,如数组或字符串。对于对象,我们可以使用对象解构来实现类似的效果:

javascript 复制代码
const originalObject = { a: 1, b: 2 };
const shallowCopy = { ...originalObject };

console.log(shallowCopy); // { a: 1, b: 2 }

在上面的代码中,我们使用���象解构将originalObject对象中的属性拆分为单独的属性,并将它们复制到shallowCopy对象中,从而实现了浅拷贝。

🦑注意事项🦑

  • 不适用于特殊类型和不可枚举属性:对于包含特殊类型(如日期、正则表达式)或不可枚举属性的对象,展开运算符可能无法正确复制,需要额外处理。

Object.assign()

Object.assign() 方法可以用于实现对象的浅拷贝。浅拷贝是指复制对象的属性值,但如果属性是引用类型(如对象或数组),则只复制其引用,而不复制实际的对象。

浅拷贝意味着只复制对象的第一层属性,而不是递归复制对象的所有嵌套属性。

js 复制代码
const source = { a: 1, b: { c: 2 } };
const shallowCopy = Object.assign({}, source);

console.log(shallowCopy); // { a: 1, b: { c: 2 } }

上面的例子中,shallowCopysource 的浅拷贝。虽然 shallowCopysource 是两个不同的对象,但是它们共享同一个 b 对象的引用。这意味着修改 shallowCopy.b 会影响到 source.b

🦑注意事项🦑

  • 只能复制可枚举属性Object.assign() 只复制源对象中可枚举的自身属性,不会复制原型链上的属性。
  • 不适用于深层嵌套对象:如果源对象的属性是对象或数组等引用类型,那么仅复制的是它们的引用,而不是创建新的独立副本。

Array.prototype.slice()

使用 Array.prototype.slice() 方法可以实现对数组的浅拷贝,但是它并不适用于对象的浅拷贝,因为它是针对数组的。

js 复制代码
const array = [1, 2, 3, 4, 5];
const shallowCopy = array.slice();

console.log(shallowCopy); // [1, 2, 3, 4, 5]

🦑注意事项🦑

  • 只适用于数组Array.prototype.slice() 方法只能用于复制数组,对于对象或其他类型的数据,它并不适用。

参考文章

JavaScript 原生深拷贝方法来啦!structuredClone 闪耀登场~

structuredClone()-MDN

相关推荐
咖啡の猫2 小时前
Shell脚本-for循环应用案例
前端·chrome
uzong3 小时前
面试官:Redis中的 16 库同时发送命令,服务端是串行执行还是并行执行
后端·面试·架构
关键帧-Keyframe4 小时前
音视频面试题集锦第 26 期
面试·音视频
百万蹄蹄向前冲4 小时前
Trae分析Phaser.js游戏《洋葱头捡星星》
前端·游戏开发·trae
朝阳5815 小时前
在浏览器端使用 xml2js 遇到的报错及解决方法
前端
GIS之路5 小时前
GeoTools 读取影像元数据
前端
ssshooter5 小时前
VSCode 自带的 TS 版本可能跟项目TS 版本不一样
前端·面试·typescript
你的人类朋友5 小时前
【Node.js】什么是Node.js
javascript·后端·node.js
Jerry6 小时前
Jetpack Compose 中的状态
前端
dae bal7 小时前
关于RSA和AES加密
前端·vue.js