前几天刷文章的时候,看到了一个用于深拷贝的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 } }
上面的例子中,shallowCopy
是 source
的浅拷贝。虽然 shallowCopy
和 source
是两个不同的对象,但是它们共享同一个 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()
方法只能用于复制数组,对于对象或其他类型的数据,它并不适用。