浅拷贝和深拷贝

304 阅读6分钟

本文内容是我从其他文章中整理而来,主要是为了自己记录一下知识点,方便日后复习。文章内容的雷同之处请大家多多谅解。

浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。

1、浅拷贝

// a原拷贝对象,b新对象 
for (const key in a) {
 b[key] = a[key] 
}

2、深拷贝

(1)简单版深拷贝,只能拷贝基本原始类型和普通对象与数组,无法拷贝循环引用

function simpleDeepClone(a) {
  const b=Array.isArray(a) ? [] : {}
  for (const key of Object.keys(a)) {
    const type = typeof a[key]
    if (type !== 'object' || a[key] === null) {
      b[key] = a[key]
    } else {
      b[key] = simpleDeepClone(a[key])
    }
  }
  return b
}

(2)精简版深拷贝,只能拷贝基本原始类型和普通对象与数组,可以拷贝循环引用

function deepClone(a, weakMap = new WeakMap()) {
  if (typeof a !== 'object' || a === null) return a
  if (s = weakMap.get(a)) return s
  const b = Array.isArray(a) ? [] : {}
  weakMap.set(a, b)
  for (const key of Object.keys(a)) b[key] = clone(a[key], weakMap)
  return b
}

(3)js原生深拷贝,无法拷贝Symbol、null、循环引用

function JSdeepClone(data) {
  if (!data || !(data instanceof Object) || (typeof data == "function")) {
    return data || undefined;
  }
  const constructor = data.constructor;
  const result = new constructor();
  for (const key in data) {
    if (data.hasOwnProperty(key)) {
      result[key] = deepClone(data[key]);
    }
  }
  return result;
}

(4)比较完善的深拷贝

因为比较复杂,所以这里先整理一下思路:

1)首先考虑简单的原始类型,先判断传入参数是否为原始类型,包括null这里归为原始类型来判断,由于原始类型在内存中保存的是值可以直接通过值的赋值操作。

2)经过原始类型的筛选,剩下对象类型,取出所有对象的键,通过Reflect.OwnKeys(obj)取出对象自身所有的键,包括Symbol的键也能取出。

3) 由于对象有2种体现形式,数组和普通对象,对于这2者要单独判断,先生成一个拷贝容器即newObj 

4)接下来就可以开始遍历 步骤2 中获取到对象所有的键(仅自身包含的键),通过for..of 遍历,取出当前要拷贝的对象a,对应于当前遍历键的值,即a[key] 

5)对a[key]值的类型进行判断,此值类型的可能性包括所有的类型,所以又回到步骤1中先判断原始类型数据;如果是原始类型可以直接赋值跳过这一轮,进行下一轮遍历

 6)经过上一步的筛选,此时剩下的只是对象类型,由于对象类型无法通过typeof直接区分,所以可以借用原始对象原型方法 Object.prototype.toString.call(obj) 来进行对象具体类型的判断 

7)toString判断的结果会以'[object xxx]',xxx为对应对象类型形式体现,基于这种转换可以清晰判断对象的具体类型,之后再对各种类型进行相应的深拷贝即可

8) 以上并未使用递归,由于上述的拷贝,还未涉及多层次的嵌套关系并不需要使用递归 

9)接下来将要判断嵌套类型数据,(此顺序可变,不过出现频率高的尽量放在前头)首先判断普通对象和数组,如果是,则直接扔给递归处理,由于处理数组和普通对象的逻辑已经在这之前处理好了,现在只需重复上面的步骤,所以直接递归调用就好,递归到最后一层,应该是原始类型的数据,不会进入无限调用 

10)接下来是判断2种特殊类型Set和Map,由于这2种类型的拷贝方式不同,进一步通过if分支对其判断,遍历里边所存放的值,Set使用add方法向新的拷贝容器添加与拷贝对象相同的值,此处值的拷贝也应该使用深拷贝,即直接把值丢给递归函数,它就会返回一个拷贝好的值。Map类似,调用set方法设置键和值,不过正好Map的键可以存放各种类型

11))到了拷贝Symbol环节,这个类型相对特殊一点,Symbol的值是唯一的,所以要获取原Symbol所对应的Symbol值,则必须通过借用Symbol的原型方法来指明要获取Symbol所对应Symbol的原始值,基于原始值创建一个包装器对象,则这个对象的值与原来相同 

12)筛选到这里,剩余的对象,基本上就是一些内置对象或者是不需要递归遍历属性的对象,那么就可以基于这些对象原型的构造函数来实例化相应的对象

13)最后遍历完所有的属性就可以返回这个拷贝后的新容器对象,作为拷贝对象的替代

14) 基于循环引用对象的解析,由于循环引用对象会造成循环递归导致调用栈溢出,所以要考虑到一个对象不能被多次拷贝。基于这个条件可以使用Map对象来保存一个拷贝对应的表,因为Map的键的特殊效果可以保存对象,因此正好适用于对拷贝对象的记录,且值则是对应的新拷贝容器,当下次递归进来的时候先在拷贝表里查询这个键是否存在,如果存在说明已经拷贝过,则直接返回之前拷贝的结果,反之继续 。

function deepClonePlus(a, weakMap = new WeakMap()) {
  const type = typeof a
  if (a === null || type !== 'object') return a
  if (s = weakMap.get(a)) return s
  const allKeys = Reflect.ownKeys(a)
  const newObj = Array.isArray(a) ? [] : {}
  weakMap.set(a, newObj)
  for (const key of allKeys) {
    const value = a[key]
    const T = typeof value
    if (value === null || T !== 'object') {
      newObj[key] = value
      continue
    }
    const objT = Object.prototype.toString.call(value)
    if (objT === '[object Object]' || objT === '[object Array]') {
      newObj[key] = deepClonePlus(value, weakMap)
      continue
    }
    if (objT === '[object Set]' || objT === '[object Map]') {
      if (objT === '[object Set]') {
        newObj[key] = new Set()
        value.forEach(v => newObj[key].add(deepClonePlus(v, weakMap)))
      } else {
        newObj[key] = new Map()
        value.forEach((v, i) => newObj[key].set(i, deepClonePlus(v, weakMap)))
      }
      continue
    }
    if (objT === '[object Symbol]') {
      newObj[key] = Object(Symbol.prototype.valueOf.call(value))
      continue
    }
    newObj[key] = new a[key].constructor(value)
  }
  return newObj
}

上面的内容涉及到一些不常用的方法,这里解释一下:

1)WeakMap()

WeakMap 对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。

由于Map存放的键属于强引用类型,且深拷贝的数据量也不小,如果这些拷贝后的拷贝表不及时释放可能会造成垃圾堆积影响性能,因此需要使用到weakMap方法代替Map。

使用弱引用的好处,可以优化垃圾回收,weakMap存放的是拷贝表,此拷贝表在拷贝完成之后就没有作用了,之前存放的拷贝对象,经过深拷贝给新拷贝容器,则这些旧对象在销毁之后,对应于拷贝表里的对象也应该随之清除,不应该还保留,这就是使用弱引用来保存表的原因。

2)Reflect.OwnKeys(obj)

此方法返回由目标对象的自身属性键组成的 Array。如果目标不是 Object,抛出一个 TypeError