真的会用深浅拷贝吗?深入理解,干倒面试官👊!

一、前言

老生常谈的话题为何还要拿出来讲,因为发现身边的朋友部分仍停留在知道且有一点实践的层面,决定撸起袖子总结一番。

不过掘金大舞台,怎会缺少深浅拷贝的优质文!

自知不如,所以打算换个角度,聊一聊深入的应用:实际应用案例循环引用

当然为了照顾还不清楚的童鞋,我们先从基础概念讲起👊(大佬们请直接跳过)

二、直接赋值、浅拷贝、深拷贝

当涉及到数据复制和赋值时,有三个常用的概念:直接赋值浅拷贝深拷贝。它们之间的区别在于复制的程度和对原始数据的影响

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 深拷贝循环引用问题

当进行深拷贝时,确实可能遇到循环引用的问题------对象或数据结构中的两个或多个元素相互引用,形成一个环状结构。

在深拷贝过程中,如果遇到循环引用,传统的深拷贝方法可能会导致无限递归,最终导致程序崩溃。这是因为传统深拷贝方法无法判断何时遇到循环引用,而是简单地递归地复制对象的属性。

为了解决循环引用问题,可以使用一些技术或库来检测和处理。以下是几种常见的解决方法:

  1. 标记已访问的对象 :在深拷贝过程中,可以使用一个集合(如 Setweakmap)来记录已经访问过的对象。当遇到循环引用时,可以检查该集合,避免无限递归。这种方法需要在深拷贝过程中进行额外的判断和处理。
  2. 限制深度:可以设置一个深度限制,当达到指定的深度时,停止递归深拷贝。这种方法可以防止无限递归,但可能会导致部分数据丢失或不完整。
  3. 使用专门的库 :一些专门的深拷贝库(如 lodashcloneDeep 方法)提供了更强大的功能来处理循环引用。

下面是一个使用标记已访问对象的方法来处理循环引用的示例代码:

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 深拷贝的性能问题

在上面的处理循环引用中,也顺带解决了一定的性能问题,但其实深拷贝的性能问题都是随数据复杂程度而定的,比如对 swaggerresponse 的数据结构解析就是相当复杂的。

这边提供一个比较少见的思路------选择性拷贝。

直接上代码:

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 }

如果大家有更好的性能优化思路,也可以提供一下呀~

六、总结

对于深浅拷贝的探究到这里就结束了,其中可能问题期待读者的指出。

建议结合更多大佬的好文细品~

相关推荐
网络安全queen2 小时前
渗透测试面试问题
面试·职场和发展
a栋栋栋4 小时前
apifox
java·前端·javascript
请叫我飞哥@4 小时前
HTML 标签页(Tabs)详细讲解
前端·html
Anlici5 小时前
React18与Vue3组件通信对比学习(详细!建议收藏!!🚀🚀)
前端·vue.js·react.js
m0_748251525 小时前
PDF在线预览实现:如何使用vue-pdf-embed实现前端PDF在线阅读
前端·vue.js·pdf
中生代技术5 小时前
3.从制定标准到持续监控:7个关键阶段提升App用户体验
大数据·运维·服务器·前端·ux
m0_748239335 小时前
从零开始:如何在.NET Core Web API中完美配置Swagger文档
前端·.netcore
m0_748232926 小时前
【前端】Node.js使用教程
前端·node.js·vim
hawleyHuo6 小时前
umi 能适配 taro组件?
前端·前端框架
web130933203986 小时前
[JAVA Web] 02_第二章 HTML&CSS
java·前端·html