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

前言

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

浅拷贝

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

什么是浅拷贝

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

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

  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 等特定的环境,并且实现较为复杂,使用时需要额外的代码和处理逻辑。

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

相关推荐
zhougl99642 分钟前
html处理Base文件流
linux·前端·html
花花鱼1 小时前
node-modules-inspector 可视化node_modules
前端·javascript·vue.js
HBR666_1 小时前
marked库(高效将 Markdown 转换为 HTML 的利器)
前端·markdown
careybobo2 小时前
海康摄像头通过Web插件进行预览播放和控制
前端
TDengine (老段)3 小时前
TDengine 中的关联查询
大数据·javascript·网络·物联网·时序数据库·tdengine·iotdb
杉之4 小时前
常见前端GET请求以及对应的Spring后端接收接口写法
java·前端·后端·spring·vue
喝拿铁写前端4 小时前
字段聚类,到底有什么用?——从系统混乱到结构认知的第一步
前端
再学一点就睡4 小时前
大文件上传之切片上传以及开发全流程之前端篇
前端·javascript
木木黄木木5 小时前
html5炫酷图片悬停效果实现详解
前端·html·html5
请来次降维打击!!!6 小时前
优选算法系列(5.位运算)
java·前端·c++·算法