通过学习🍊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

相关推荐
飞翔的猪猪3 分钟前
GitHub Recovery Codes - 用于 GitHub Two-factor authentication (2FA) 凭据丢失时登录账号
前端·git·github
前端开发熊8 分钟前
实时薪资追踪-每秒都让收入看得见的 Chrome 扩展,你还不来试试?
前端
bnnnnnnnn10 分钟前
看完就懂、懂完就敢讲的「原型与原型链」终极八卦!
前端·javascript·面试
zacksleo12 分钟前
哪些鸿蒙原生应用在使用Flutter
前端·flutter·harmonyos
水煮白菜王13 分钟前
Nginx攻略
前端·nginx
難釋懷20 分钟前
Vue非单文件组件
前端·vue.js
byte轻骑兵25 分钟前
蓝牙 BLE 扫描面试题大全(2):进阶面试题与实战演练
面试·职场和发展
克里斯前端32 分钟前
vue在打包的时候能不能固定assets里的js和css文件名称
javascript·css·vue.js
恰薯条的屑海鸥35 分钟前
零基础学前端-传统前端开发(第三期-CSS介绍与应用)
前端·css·学习·css3·前端开发·前端入门·前端教程
海盐泡泡龟36 分钟前
盒模型小全
前端·css·盒模型