你还不知道深拷贝与浅拷贝的区别吗?

一、前言

在 JavaScript 开发中,深拷贝和浅拷贝是处理对象和数组的常见操作。它们涉及到复制数据结构以及处理引用类型数据的方式。

深拷贝和浅拷贝在不同的场景中有各自的应用。浅拷贝适用于简单的数据结构,当我们只需要复制基本数据类型的值或者共享引用类型数据时。而深拷贝则适用于需要完全独立的副本,以避免原始数据的修改对拷贝对象造成影响的情况。

在实际开发中,我们需要根据具体的需求和数据结构选择适当的拷贝方式。对于简单的数据结构,浅拷贝可能足够满足需求,而对于复杂的嵌套对象或数组,深拷贝则更为安全可靠。
了解深拷贝和浅拷贝的概念和区别,可以帮助我们在处理数据时选择合适的拷贝方式,确保数据的正确性和一致性。

二、两种不同的拷贝方式

(1)浅拷贝

浅拷贝是创建一个新的对象或数组,并将原始数据的值复制到新对象中。当原始数据是基本数据类型(如数字、字符串、布尔值)时,浅拷贝会直接复制其值。然而,当原始数据是引用类型(如对象或数组)时,浅拷贝只会复制引用,而不是创建新的独立副本。这意味着新对象和原始对象会共享同一份数据,对其中一个对象的修改会影响到另一个对象。

这里我们创建一个obj对象,并且将obj对象赋值给obj1,当我们改变obj1的name属性和stu属性里的name属性后,同时打印obj和obj1对象,看看有什么区别。

const s = Symbol()
const obj = {
  name:'aaa',
  age:18,
  run:function(){
    console.log(111)
  },
  s:'xxx',
  stu:{
    name:'bbb',
    age:19,
    eat:function(){
      console.log('eating')
    }
  }
}

const obj1 = obj
obj1.name = 'bbb'
obj1.stu.name = 'ccc'
console.log(obj1,obj)

你还不知道深拷贝与浅拷贝的区别吗?_第1张图片
我们发现修改obj1后,原来的obj也会被修改,这是怎么回事呢?这里可以画出obj和obj1的内存图。
你还不知道深拷贝与浅拷贝的区别吗?_第2张图片
从图中我们清楚地看出,obj和obj1指向的内存地址是同一个,当其中一个对象修改属性时,所指向它内存地址的对象就会受到影响。
这就是浅拷贝,常见浅拷贝的方式有以下:

  1. 直接赋值
  2. 使用…扩展运算符
  3. Object.assign()
  4. 数组方法:slice和concat方法

(2)深拷贝

深拷贝是创建一个全新的对象或数组,并递归地复制原始数据中的所有值和嵌套对象。深拷贝会创建独立的副本,使得新对象和原始对象完全独立,互不影响。最常见的深拷贝方式使用JSON.parse(JSON.stringify())。我们通过一个例子进行验证一下。

const s = Symbol()
const obj = {
  name:'aaa',
  age:undefined,
  date:new Date(),
  run:function(){
    console.log(111)
  },
  s:'xxx',
  stu:{
    name:'bbb',
    age:19,
    eat:function(){
      console.log('eating')
    }
  }
}

const obj1 = JSON.parse(JSON.stringify(obj))
obj1.name = 'bbb'
obj1.stu.name = 'ccc'
console.log(obj1,obj)
console.log(obj1.date.getTime())

你还不知道深拷贝与浅拷贝的区别吗?_第3张图片
可以看出,obj1更改的两个属性均不会影响到源对象,因为当我们使用深拷贝的方式的时候,会先创建一份新的内存,两个内存地址不同,自然不会影响到源对象。但是当我们使用JSON.parse(JSON.stringify())克隆对象时,从上图发现以下几点:

  1. 当属性值为函数或者undefined时,会被忽略。
  2. 当属性值为Date类型时,克隆对象调用方法将报错。

所以使用JSON.parse(JSON.stringify())克隆对象需要谨慎考虑,那么现在我们可以封装一个通用方法,实现能够真正深拷贝对象的功能。

深拷贝封装:

  1. 创建一个函数,接收源对象参数obj。
  2. 判断当前对象是否为对象类型,若不为对象直接返回obj。
  3. 判断当前对象是否为Date类型,若为Date类型,则通过new Date(obj)创建新的Date对象。
  4. 判断当前对象是否为函数类型,函数类型不进行深拷贝,直接返回该函数。
  5. 创建一个拷贝变量,通过Array.isArray判断是否为数组类型,是的话赋值为空数组,否则赋值为对象。
  6. 递归遍历当前对象的属性值
  7. 最后返回拷贝变量
function deepClone(obj){
  // 判断是否为对象
  if(obj !== null && (typeof obj !== "object" || typeof obj !=="function")){
    return obj
  }
  // 判断是否为日期类型
  if(obj instanceof Date){
    return new Date(obj)
  }
  // 判断是否为函数,不进行深拷贝
  if(obj instanceof Function){
    return obj
  }

  let newObj = Array.isArray(obj) ? [] : {}

  for(const key in obj){
    newObj[key] = deepClone(obj[key])
  }
  return newObj
}

封装完测试一下吧!
你还不知道深拷贝与浅拷贝的区别吗?_第4张图片
可以看出基本解决了上述的问题,但是,若一个属性的属性值为当前对象,造成循环引用,我们将obj的stu属性的属性值赋值为obj,会发现“爆栈”的错误。解决问题的方法是使用一个WeakMap来保存拷贝过的对象,检查到当前对象已经拷贝过,直接从WeakMap中获取。(使用WeakMap原因是键必须为对象且当没有对象引用,会自动进行垃圾回收)。

function deepClone(obj, wm = new WeakMap()) {
  // 判断是否为对象
  if (obj !== null && (typeof obj !== "object" || typeof obj !== "function")) {
    return obj
  }
  // 检查是否已经拷贝过该对象
  if (wm.has(obj)) {
    return wm.get(obj)
  }

  // 判断是否为日期类型
  if (obj instanceof Date) {
    return new Date(obj)
  }
  // 判断是否为函数,不进行深拷贝
  if (obj instanceof Function) {
    return obj
  }

  let newObj = Array.isArray(obj) ? [] : {}
  // 将新对象添加到已访问的映射中
  wm.set(obj, newObj)

  for (const key in obj) {
    newObj[key] = deepClone(obj[key], wm)
  }
  return newObj
}

以上就是深拷贝和浅拷贝的相关知识点,看完这篇文章,你还不懂吗?若有疑问,欢迎评论区留言。

欢迎关注公众号—代码分享站
你还不知道深拷贝与浅拷贝的区别吗?_第5张图片

你可能感兴趣的:(JavaScript,javascript,前端)