带你深入了解深浅拷贝的实现!!!

前言

在日常使用中,其实我们也是经常的在使用着拷贝的相关概念,对于项目开发来说了解深浅拷贝的有关知识是很重要的。因此在面试中对于深浅拷贝的原理及实现,是会经常被问到的,下面本人将带大家深入的了解一下,深浅拷贝的概念及其实现方法。

浅拷贝

我们将要了解的是什么是浅拷贝,为什么需要浅拷贝,浅拷贝要怎么去实现?

什么是浅拷贝

浅拷贝是指创建一个新的对象,这个对象有着原始对象的一层属性的副本。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是引用类型,拷贝的就是内存地址,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。

那么我们为什么需要使用到浅拷贝呢

  1. 防止对象引用的副作用: 在 JavaScript 中,我们都知道对象是通过引用来传递的。如果我们直接将一个对象赋值给另一个变量,那么它们将指向同一个内存地址。这就意味着,如果修改其中一个对象,另一个对象也会受到影响。而浅拷贝可以创建一个新的对象,使得修改其中一个对象不会影响到另一个对象。

    但是我们需要注意的是:浅拷贝只会复制对象的一层属性,而不会递归地复制所有嵌套对象的属性。这意味着,如果原始对象中包含了深层嵌套的对象或数组,浅拷贝无法完整地复制这些嵌套对象,而只是复制了它们的引用。因此,修改浅拷贝对象中的嵌套对象的属性会影响到原始对象。

  2. 数据独立性: 有时候我们需要对对象进行操作,但又不想影响到原始对象。这种情况下,浅拷贝可以为我们创建一个对象的副本,使得我们可以在副本上进行操作而不影响原始对象。

那么我们在日常中哪些方面会使用它呢?

  1. 对象复制: 当我们需要对一个对象进行复制,但又不想改变原始对象时,就会使用浅拷贝。例如,创建一个对象的备份以备份数据,或者创建一个对象的副本以进行修改而不影响原始对象。
  2. 传递参数: 在函数调用中,如果我们需要传递一个对象,并且希望在函数内部对该对象进行修改,但又不想修改原始对象,那么就可以使用浅拷贝来传递参数。这样可以保证原始对象的数据不会被改变。

实现方法

对于浅拷贝来说,我们有如下的实现方法:

  • for in
  • Object.assign()
  • [...arr]
  • slice()
  • concat()

for in 遍历实现

js 复制代码
Object.prototype.abc = 123
function shallowCopy(obj) {
  let newObj = {};
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      newObj[key] = obj[key];

    }
  }
  return newObj;
}
let obj = {
  a: [1,2,[3,4]],
  b: {
    c: 2,
    d: 3
  }
}
let obj2 = shallowCopy(obj);
obj.b.c = 4;
console.log(obj2);

如以上代码,我们可以通过for in遍历源对象,通过hasOwnProperty查找对象中是否有key,有的话就可以将obj里面的属性赋给newObj中,因为是赋值操作,我们可以看到,它将newObj的引用地址指向了obj的引用地址,因此实现了一个浅拷贝。

使用 Object.assign()

js 复制代码
function shallowCopy(obj) {
  return Object.assign({}, obj);
}

const obj = { name: '张三', age: 30 };
const newObj = shallowCopy(obj);

console.log(newObj); // { name: '张三', age: 30 }

因为Object.assign() 方法可以用于将一个或多个源对象的可枚举属性复制到目标对象,并返回目标对象。因此当目标对象是一个空对象时,相当于进行了浅拷贝。它会遍历源对象的可枚举属性,并将它们复制到目标对象中,但是只会复制对象的自身属性,不会复制原型链上的属性。

使用 [...arr]

js 复制代码
function shallowCopy(arr) {
  return [...arr];
}

const arr = [1, 2, 3];
const newArr = shallowCopy(arr);

console.log(newArr); // [1, 2, 3]

因为扩展运算符 ... 可以将一个数组展开为多个元素。因此当用于数组时,它会将数组中的每个元素分别取出,并放入一个新的数组中,从而实现了浅拷贝。但是这种方法适用于数组的浅拷贝,不能用于对象的拷贝。

使用 slice()

js 复制代码
function shallowCopy(arr) {
  return arr.slice();
}

const arr = [1, 2, 3];
const newArr = shallowCopy(arr);

console.log(newArr); // [1, 2, 3]

我们都知道slice() 方法会返回一个新的数组,包含从开始到结束(不包括结束,也就是左闭右开)选择的数组的浅拷贝部分。当不传入任何参数时,slice() 方法会复制整个数组,相当于进行了浅拷贝。但是该方法也只复制了数组的一层内容,不会递归复制嵌套数组或对象。

使用 concat()

js 复制代码
function shallowCopy(arr) {
  return arr.concat();
}

const arr = [1, 2, 3];
const newArr = shallowCopy(arr);

console.log(newArr); // [1, 2, 3]

concat() 方法可以用于合并两个或多个数组,并返回一个新的数组。因此当不传入任何参数时,concat() 方法会复制调用它的数组,相当于进行了浅拷贝。这种方法也只会复制数组的一层内容,不会递归复制嵌套数组或对象。

深拷贝

对于深拷贝,我们也要去了解什么是深拷贝,为什么需要深拷贝,深拷贝要怎么去实现?

什么是深拷贝

深拷贝是指在拷贝对象或数组时,将其所有层级的子对象和子数组都复制到新的对象或数组中,而不是简单地复制引用。换句话说,深拷贝会递归地复制对象及其所有子对象,从而完全独立于原始对象,修改深拷贝后的对象不会影响到原始对象。

那么我们为什么需要深拷贝呢?

主要有以下几个原因:

  1. 防止对象属性共享: 在 JavaScript 中,对象和数组都是引用类型,当我们对一个对象进行浅拷贝后,新对象和原对象共享同一份内存地址,修改新对象的属性会影响到原对象,这可能导致意外的副作用。深拷贝可以避免这种情况,确保新对象和原对象完全独立。
  2. 处理嵌套对象和数组: 当对象或数组中包含了嵌套的对象或数组时,浅拷贝无法完整地复制所有层级的子对象和子数组,而只是复制了它们的引用。这意味着,修改新对象或数组中的嵌套对象或数组会影响到原始对象或数组。深拷贝可以递归地复制所有层级的子对象和子数组,从而解决了这个问题。
  3. 保留对象的原型链关系: 深拷贝不仅会复制对象自身的属性,还会复制对象的原型链上的属性。这意味着,深拷贝后的对象与原始对象具有相同的原型链关系,可以保留对象的继承关系。

那么我们在日常中哪些方面会使用它呢?

深拷贝在日常开发中被广泛应用,特别是在处理复杂数据结构时更为常见。以下是一些常见的使用场景:

  1. 数据缓存与状态管理: 在前端开发中,经常需要对数据进行缓存或状态管理,深拷贝可以确保缓存的数据与原始数据完全独立,不会相互影响,从而保持数据的一致性和可靠性。
  2. 数据传递与组件通信: 在组件化开发中,经常需要将数据传递给子组件或通过事件总线进行组件间通信,深拷贝可以确保传递的数据在不同组件之间保持独立,避免意外修改导致的数据错误。
  3. 处理网络请求和异步操作: 在处理网络请求或异步操作返回的数据时,深拷贝可以确保操作的数据与原始数据完全独立,不会因为修改导致数据错乱或不一致。
  4. 处理复杂数据结构: 当数据结构较为复杂,包含嵌套对象或数组时,深拷贝可以递归地复制所有层级的子对象和子数组,确保数据的完整性和一致性。

实现方法

对于深拷贝,我们有如下实现方法:

  • 递归实现
  • JSON.parse(JSON.stringify(obj))
  • structedClone()
  • MessageChannel()

递归实现

递归实现是一种简单而常见的深拷贝方法。它通过递归地遍历对象的所有属性,并为每个属性创建一个新的对象或数组,从而实现深层次的复制。

js 复制代码
function deepCopy(obj) {
  let obj2 = {};
  for (let i in obj) {
    if (obj[i] instanceof Array) {
      obj2[i] = [];
      for (let j = 0; j < obj[i].length; j++) {
        obj2[i][j] = obj[i][j];
      }
    }else if (typeof obj[i] === 'object') {
      obj2[i] = deepCopy(obj[i]);
    } else {
      obj2[i] = obj[i];
    }
  }
  return obj2;
}
let obj = {
  a: [1,2,[3,4]],
  b: {
    c: 2
  }
}
let obj2 = deepCopy(obj);
// obj.b.c = 4;
console.log(obj2);

从以上代码中我们可以看出,递归实现的深拷贝函数 deepCopy 遍历对象的所有属性,并对每个属性的值进行深拷贝。如果属性值是对象或数组,则递归地调用 deepCopy 函数来复制它们的子属性。这样就可以确保所有层级的子对象和子数组都被完整地复制,实现了深拷贝。

JSON.parse(JSON.stringify(obj))

js 复制代码
let obj = {
  a: 1,
  b: {
    c: 2
  }
}

const newObj = JSON.parse(JSON.stringify(obj));
console.log(newObj);

在上面的代码中,JSON.stringify(obj) 将对象序列化为 JSON 字符串,然后 JSON.parse() 将 JSON 字符串解析为新的对象,从而实现了深拷贝。

structuredClone()

structuredClone() 方法是 HTML Living Standard 中的一个新 API,它可以用于复制复杂的数据结构,包括对象、数组、Map、Set、Blob、File 等。这个方法主要用于 Web Worker 等环境中,在主线程和工作线程之间传递数据。对于 Web Worker 后面我们会做详细的介绍。

js 复制代码
let obj = {
  a: 1,
  b: {
    c: 2
  }
}

const newObj = structuredClone(obj);
console.log(newObj);

structuredClone() 方法通过深度遍历对象及其所有的属性和子属性,并使用适当的复制算法来复制它们。这样就实现了完整的深拷贝。

MessageChannel()

MessageChannel() 是一个 HTML5 中的 API,它虽然主要用于在不同的窗口或 iframe 之间进行通信,但是也可以用于实现深拷贝。

js 复制代码
let obj = {
  a: 1,
  b: {
    c: 2
  },
  d:undefined,
}


function deepClone(obj) {
  return new Promise((resolve) => {
    const { port1, port2 } = new MessageChannel();
    port1.postMessage(obj);
    port2.onmessage = (msg) => {
      resolve(msg.data);
    }
  })
}

let obj2 = null;
deepClone(obj).then((res) => {
  console.log(res);
  obj2 = res;
  obj.b.c = 3;
  console.log(obj2);
})

在上面的代码中,我们创建了一个 MessageChannel,并从中获得了两个 MessagePort 对象 port1port2。 然后使用 port1.postMessage(obj) 将原始对象 obj 发送到 port1。在 port2.onmessage 事件处理程序中,接收到了从 port1 发送过来的对象,并将其解析为新的对象,最后通过 resolve 返回新对象。在 deepClone 函数中使用了 Promise 包装整个过程,以便在异步操作完成后获取新对象。

因为 MessageChannel() 在不同线程之间传递数据时,会执行序列化和反序列化操作,所以即使原始对象发生改变,新对象不会受到影响,从而实现了深拷贝。

总结

浅拷贝

for in 遍历:

可以遍历对象的所有属性,并对每个属性进行复制。但是有着明显的缺点:只复制对象的一层属性,不会递归复制嵌套对象。如果原始对象包含嵌套对象或数组,浅拷贝无法完整地复制这些嵌套结构,而只是复制了它们的引用。因此,修改浅拷贝对象中的嵌套对象的属性会影响到原始对象。

Object.assign():

可以将一个或多个源对象的可枚举属性复制到目标对象。但是缺点是同样只复制对象的一层属性,不会递归复制嵌套对象。因此,对于包含嵌套结构的对象,使用 Object.assign() 也无法完整地复制所有层级的子对象。

扩展运算符 [...arr]

可以将一个数组展开为多个元素,并放入一个新的数组中。但是适用于数组的浅拷贝,但不能用于对象的拷贝。无法复制对象的属性,只能复制数组的内容。

slice() 方法:

可以返回一个新的数组,包含从开始到结束选择的数组的浅拷贝部分。虽然 slice() 方法可以复制整个数组,但它也只复制了数组的一层内容,不会递归复制嵌套数组或对象。

concat() 方法:

可以通过合并两个或多个数组,并返回一个新的数组来实现浅拷贝。但是与 slice() 方法类似,虽然 concat() 方法可以复制整个数组,但它也只复制了数组的一层内容,不会递归复制嵌套数组或对象。

深拷贝

递归实现:

可以通过递归地遍历对象的所有属性,并对每个属性进行深拷贝。但是实现较为复杂,需要递归地遍历对象的所有属性,并对每个属性进行深拷贝。对于包含循环引用或大型对象的情况,性能消耗较大,可能会导致栈溢出或内存泄漏。

JSON.parse(JSON.stringify(obj))

可以通过将对象序列化为 JSON 字符串,然后再解析为新的对象来实现深拷贝。此方法虽然可以简单地通过序列化和反序列化操作实现深拷贝,但该方法对于一些特殊的对象类型,如函数、正则表达式、Date 等,无法被完整地复制。此外,该方法也无法处理包含循环引用的对象,会导致堆栈溢出。

structuredClone()

可以通过深度遍历对象及其所有的属性和子属性,并使用适当的复制算法来复制它们以此实现深拷贝。虽然 structuredClone() 方法可以处理复杂的数据结构,并适用于大多数对象类型,但它在某些环境下可能不受支持(如旧版浏览器),并且在某些情况下可能存在性能问题。

MessageChannel()

可以在不同线程之间传递数据时,执行序列化和反序列化操作,实现完全独立于原始对象的深拷贝。虽然 MessageChannel() 可以在不同线程之间传递数据,并执行序列化和反序列化操作,从而实现深拷贝,但它仅适用于 Web Worker 等特定的环境,并且实现较为复杂,使用时需要额外的代码和处理逻辑。

综上所述,每种浅拷贝和深拷贝方法都有其特定的缺点,需要根据具体情况选择最合适的方法来进行对象或数组的拷贝。

相关推荐
ThisIsClark几秒前
【后端面试总结】MySQL主从复制逻辑的技术介绍
mysql·面试·职场和发展
m0_748254882 分钟前
DataX3.0+DataX-Web部署分布式可视化ETL系统
前端·分布式·etl
ZJ_.14 分钟前
WPSJS:让 WPS 办公与 JavaScript 完美联动
开发语言·前端·javascript·vscode·ecmascript·wps
GIS开发特训营18 分钟前
Vue零基础教程|从前端框架到GIS开发系列课程(七)响应式系统介绍
前端·vue.js·前端框架·gis开发·webgis·三维gis
Cachel wood44 分钟前
python round四舍五入和decimal库精确四舍五入
java·linux·前端·数据库·vue.js·python·前端框架
学代码的小前端1 小时前
0基础学前端-----CSS DAY9
前端·css
joan_851 小时前
layui表格templet图片渲染--模板字符串和字符串拼接
前端·javascript·layui
程序猿进阶1 小时前
深入解析 Spring WebFlux:原理与应用
java·开发语言·后端·spring·面试·架构·springboot
还是大剑师兰特1 小时前
什么是尾调用,使用尾调用有什么好处?
javascript·大剑师·尾调用
m0_748236111 小时前
Calcite Web 项目常见问题解决方案
开发语言·前端·rust