一、前言
老生常谈的话题为何还要拿出来讲,因为发现身边的朋友部分仍停留在知道且有一点实践的层面,决定撸起袖子总结一番。
不过掘金大舞台,怎会缺少深浅拷贝
的优质文!
自知不如,所以打算换个角度,聊一聊深入的应用:实际应用案例
、循环引用
。
当然为了照顾还不清楚的童鞋,我们先从基础概念讲起👊(大佬们请直接跳过)
二、直接赋值、浅拷贝、深拷贝
当涉及到数据复制和赋值时,有三个常用的概念:直接赋值
、浅拷贝
和深拷贝
。它们之间的区别在于复制的程度和对原始数据的影响 。
ps:相关的拷贝方法后面会详细说的🫡
2.1 直接赋值
直接赋值
将一个变量或对象的引用赋给另一个变量或对象。这意味着两个变量或对象引用相同的内存地址 ,它们指向同一个地址
。当一个变量或对象发生改变时,另一个变量或对象也会受到影响。
js
const obj1 = {name:'Uaena'}
const obj2 = obj1 // 直接赋值
obj1.age = 20
console.log(obj2) // {"name":"Uaena","age":20}
2.2 浅拷贝
浅拷贝
创建一个新的变量或对象,并复制原始数据的值。但是,如果原始数据是引用类型,浅拷贝只会复制其地址
而不是实际的数据。这意味着新对象和原始对象仍然共享相同的引用,当一个对象发生改变时,另一个对象也会受到影响。
js
let obj1 = { name: "Uaena", hobbies: ["coding", "playing"] };
let obj2 = Object.assign({}, obj1); // 浅拷贝
obj1.hobbies.push("working");
console.log(obj2.hobbies); // coding,playing,working
2.3 深拷贝
深拷贝
是一种完全复制数据的方式,它创建一个新的变量或对象,并递归地
复制原始数据的所有值和嵌套对象。深拷贝
不共享引用,因此对一个对象的修改不会影响到另一个对象。
js
let obj1 = { name: "Uaena", hobbies: ["coding", "playing"] };
let obj2 = JSON.parse(JSON.stringify(obj1)); // 深拷贝
obj1.hobbies.push("working");
console.log(obj2.hobbies); // coding,playing
综上,我们通过一个表格来更好的进行比对:
特点 | 直接赋值 | 浅拷贝 | 深拷贝 |
---|---|---|---|
和原数据指向同一地址 | 是 | 否 | 否 |
对基本类型共享地址 | 是 | 否 | 否 |
对引用类型共享地址 | 是 | 否 | 否 |
性能开销 | 无(直接引用) | 较小 | 较大 |
三、浅拷贝实现方案
咱们把除 lodash
以外的常用方法在这里罗列一下(loash
的实现方法放在后面搞)
3.1 Object.assign()
Object.assign(target, ...sources)
方法可以用于浅拷贝对象。它接收一个目标对象和一个或多个源对象作为参数,并将源对象的属性复制到目标对象中。
当属性出现重复时,原则为后面覆盖前面
js
let obj1 = { name: "Uaena", hobbies: ["coding", "playing"] };
let obj2 = {}
Object.assign(obj2, obj1, { age: 11 });
obj1.hobbies.push("working");
console.log(obj2);
// {"name":"Uaena","hobbies":["coding","playing","working"],"age":11}
3.2 Spread Operator(展开运算符)
展开运算符...
可以用于浅拷贝数组和对象。通过将数组或对象展开为独立的元素,可以创建一个新的数组或对象。
js
let obj1 = { name: "Uaena", hobbies: ["coding", "playing"] };
let obj2 = { ...obj1 }; // 使用展开运算符进行浅拷贝
obj1.hobbies.push("working")
console.log(obj2.hobbies); // coding,playing,working
3.3 Array.prototype.slice()
slice()
方法可用于浅拷贝数组。它接收起始索引和结束索引作为参数,并返回一个新的数组,其中包含从起始索引到结束索引的元素。
js
let arr1 = [1, 2, [1, 2]];
let arr2 = arr1.slice(); // 使用slice()进行浅拷贝
arr1[2].push(3)
console.log(arr2); // [1,2,[1,2,3]]
3.4 Array.prototype.concat()
concat()
方法也可用于浅拷贝数组。它接收一个或多个数组作为参数,并返回一个新的数组,其中包含原始数组和传入数组的所有元素。
js
const array1 = [1, 2, [3]];
const array2 = [4, 5, 6];
const array3 = array1.concat(array2);
array1[2].push(3)
console.log(array3);// [1,2,[3,3],4,5,6]
千万注意!!!太多太多开发者会在各种业务场景中踩到拷贝的坑,面对复杂的数据类型 ,一定不要拿来就浅拷贝
!
四、深拷贝实现方案
同样的,lodash
的实现方案我们放在后面讲。
4.1 手动递归复制
这是一种基本的深拷贝方法,适用于对象和数组。它通过递归遍历对象的属性或数组的元素,并创建它们的副本。可以使用循环和条件语句来实现此操作。
js
function deepCopy(obj) {
// 若 obj 不是对象、数组 或为 null,直接返回 obj
if (typeof obj !== 'object' || obj === null) {
return obj;
}
let copy = Array.isArray(obj) ? [] : {};
for (let key in obj) {
// hasOwnProperty() 判断 key 是否是自身属性而非继承属性
if (obj.hasOwnProperty(key)) {
copy[key] = deepCopy(obj[key]);
}
}
return copy;
}
let original = { name: "Uaena", age: 20, hobbies: ['coding', 'playing'] };
let copy = deepCopy(original);
original.hobbies.push('working')
console.log(copy); // {"name":"Uaena","age":20,"hobbies":["coding","playing"]}
4.2 手动递归复制
通过递归也可以实现深拷贝(跟树一样)。
js
function deepCopy(value) {
let stack = [{ source: value, target: Array.isArray(value) ? [] : {} }];
let clone = stack[0].target;
while (stack.length) {
let { source, target } = stack.pop();
for (let key in source) {
if (source.hasOwnProperty(key)) {
let val = source[key];
if (typeof val === 'object') {
target[key] = Array.isArray(val) ? [] : {};
stack.push({ source: val, target: target[key] });
} else {
target[key] = val;
}
}
}
}
return clone;
}
let original = { name: "Uaena", age: 20, hobbies: ['coding', 'playing'] };
let copy = deepCopy(original);
original.hobbies.push('working')
console.log(copy); // {"name":"Uaena","age":20,"hobbies":["coding","playing"]}
4.3 JSON 序列化和反序列化
该方法将对象转换为 JSON
字符串,然后再将其解析回对象。这样可以创建一个新的对象,与原始对象完全独立。
js
let original = { name: "Uaena", age: 20, hobbies: ['coding', 'playing'] };
let copy = JSON.parse(JSON.stringify(original));
original.hobbies.push('working')
console.log(copy); // {"name":"Uaena","age":20,"hobbies":["coding","playing"]}
需要注意的是,JSON
序列化和反序列化可能无法处理一些特殊数据类型,例如函数、正则表达式、Date
对象等,因为它们在序列化和反序列化过程中会丢失其原始类型。
补充一下:其实还有优秀的第三方库 lodash
可以实现深浅拷贝,大家可以去看看文档说明~
五、🌟🌟🌟深入应用
这里才是文章的精华!!这一部分我会讲一些深浅拷贝的实际应用
,以及如何做性能优化
、与不可变数据结构结合
。
5.1 实际应用案例------数据缓存和快照
在某些情况下,我们可能需要对数据进行缓存
或创建数据的快照
,以便在需要时可以恢复到先前的状态。在这种情况下,深拷贝非常有用,可以创建数据的完全独立副本,而不会受到原始数据的后续更改的影响。
常见的场景:多步骤表单数据存储、文档编辑器的撤销和重做、游戏进度保存和加载等
js
let originalData = { name: "Uaena", age: 20, hobbies: ['coding', 'playing'] }
let dataSnapshot = JSON.parse(JSON.stringify(originalData));
// 修改原始数据
originalData.age = 21;
originalData.hobbies.push('working');
console.log(originalData);
// {"name":"Uaena","age":21,"hobbies":["coding","playing","working"]}
console.log(dataSnapshot);
// {"name":"Uaena","age":20,"hobbies":["coding","playing"]}
5.2 实际应用案例------数据传递和函数调用
在函数调用过程中,我们可能需要传递对象或数组作为参数。如果我们希望避免在函数内部修改传递的参数,我们可以使用深浅拷贝来创建参数的副本。这样可以确保我们在函数内部对副本进行修改时不会影响原始数据。
js
// 使用深拷贝创建参数的副本
function processArray(arr) {
// 使用JSON.parse和JSON.stringify进行深拷贝
const arrCopy = JSON.parse(JSON.stringify(arr));
// 对副本进行修改,不会影响原始数据
arrCopy.push({ name: 'New Object' });
// 打印副本和原始数据
console.log('副本:', arrCopy);
console.log('原始数据:', arr);
}
// 原始数组
const originalArray = [{ name: 'Object 1' }, { name: 'Object 2' }];
// 调用函数并传递原始数组作为参数
processArray(originalArray);
当然在数据结构简单的情况下优先考虑浅拷贝实现以提高性能。
5.3 深拷贝循环引用问题
当进行深拷贝时,确实可能遇到循环引用
的问题------对象或数据结构中的两个或多个元素相互引用,形成一个环状结构。
在深拷贝过程中,如果遇到循环引用
,传统的深拷贝方法可能会导致无限递归,最终导致程序崩溃。这是因为传统深拷贝方法无法判断何时遇到循环引用,而是简单地递归地复制对象的属性。
为了解决循环引用
问题,可以使用一些技术或库来检测和处理。以下是几种常见的解决方法:
- 标记已访问的对象 :在深拷贝过程中,可以使用一个集合(如
Set
、weakmap
)来记录已经访问过的对象。当遇到循环引用时,可以检查该集合,避免无限递归。这种方法需要在深拷贝过程中进行额外的判断和处理。 - 限制深度:可以设置一个深度限制,当达到指定的深度时,停止递归深拷贝。这种方法可以防止无限递归,但可能会导致部分数据丢失或不完整。
- 使用专门的库 :一些专门的深拷贝库(如
lodash
的cloneDeep
方法)提供了更强大的功能来处理循环引用。
下面是一个使用标记已访问对象的方法来处理循环引用的示例代码:
js
function deepCopy(obj, visited = new Set()) {
// 检查是否已经访问过该对象
if (visited.has(obj)) {
return obj; // 如果已经访问过,则直接返回该对象,避免无限递归
}
// 创建一个新的副本对象
const copy = Array.isArray(obj) ? [] : {};
// 将对象添加到已访问集合中
visited.add(obj);
// 遍历对象的属性并进行深拷贝
for (let key in obj) {
if (obj.hasOwnProperty(key)) { // 检查是否是自身属性
if (typeof obj[key] === 'object' && obj[key] !== null) {
copy[key] = deepCopy(obj[key], visited); // 递归深拷贝
} else {
copy[key] = obj[key]; // 直接复制非对象属性
}
}
}
return copy;
}
// 创建一个循环引用的对象
const obj = { name: 'Uaena' };
obj.ref = obj;
// 深拷贝对象
const copiedObj = deepCopy(obj);
console.log(copiedObj);
//{"name":"Uaena","ref":{"name":"Uaena","ref":......}}
在上述代码中,我们定义了一个deepCopy
函数,其中使用了一个visited
集合来存储已经访问过的对象。在深拷贝过程中,我们先检查是否已经访问过该对象,如果是,则直接返回该对象,避免无限递归。然后,我们遍历对象的属性,并进行深拷贝。
然而我们会发现 ref
中会有无限嵌套 obj
的情况,可以用使用引用对象
使之变得好看点(类似于 swagger
,即下图中的 $ref
)
5.4 深拷贝的性能问题
在上面的处理循环引用中,也顺带解决了一定的性能问题,但其实深拷贝的性能问题都是随数据复杂程度而定的,比如对 swagger
的 response
的数据结构解析就是相当复杂的。
这边提供一个比较少见的思路------选择性拷贝。
直接上代码:
js
// 选择性拷贝对象的属性
function selectiveCopy(obj, properties) {
const result = {};
for (let prop of properties) {
if (obj.hasOwnProperty(prop)) {
result[prop] = obj[prop];
}
}
return result;
}
// 示例使用
const sourceObj = { name: "Uaena", age: 20, city: "xxx" };
const propertiesToCopy = ["name", "age"];
const copiedObj = selectiveCopy(sourceObj, propertiesToCopy);
console.log(copiedObj); // 输出:{ name: "Uaena", age: 20 }
如果大家有更好的性能优化思路,也可以提供一下呀~
六、总结
对于深浅拷贝的探究到这里就结束了,其中可能问题期待读者的指出。
建议结合更多大佬的好文细品~