手写深拷贝

82 阅读2分钟

这个是我们要拷贝的对象obj 包含了多种类型

function fn () {}
fn.prototype.abc = 'abc'

const obj = {
  a: 1,
  b: 'b',
  c: {
    d: true
  },
  e: function () {
    console.log('this is e');
  },
  f: [
    {aa: 1},{bb:2}
  ],
  g: undefined,
  h: null,
  i: Symbol,
  j: new fn(),
}

先来看一下实现浅拷贝有几种方法

  • 第一种方法可以使用Object.assign()
const copyObj = Object.assign({}, obj)
console.log(copyObj === obj)
// false
obj.c.d = 1
console.log(copyObj.c.d)
// 1
  • 第二种方法使用展开运算符
const copyObj = {...obj}
console.log(copyObj === obj)
// false
obj.c.d = 1
console.log(copyObj.c.d)
// 1
  • 第三种方法使用循环负值
const copyObj = {}
for (const i in obj) {
  if (obj.hasOwnProperty(i)) {
    copyObj[i] = obj[i]
  }
}
// false
obj.c.d = 1
console.log(copyObj.c.d)
// 1

接下来我们来看一下深拷贝

  • 第一种办法是使用JSON.stringify转换成字符串 然后JSON.parse再转成对象
const copyObj3 = JSON.parse(JSON.stringify(obj))
/*
    { a: 1,
      b: 'b',
      c: { d: true },
      f: [ { aa: 1 }, { bb: 2 } ],
      h: null,
      j: {} 
    }
*/
console.log(copyObj3.j.abc)
// undefined

缺点:
1 .symbol undefind function 无法被转换 因为这三种类型在进行JSON.stringify的时候会过滤掉 并且无法序列化不可枚举属性
2. 经过转换后的对象无法继承原型链上的属性

  • 第二种方法使用递归调用赋值
function deepClone(obj) {
  // 判断是否是null或者不是对象类型
  if (typeof obj !== 'object' || obj === null) {
    return obj
  }
  let newObj = {}
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      // 判断是数组还是对象
      if (obj[key] instanceof Array) {
        // 如果是数组循环数组 递归调用 因为数组元素有可能是对象
        newObj[key] = []
        obj[key].forEach((e, k) => {
          newObj[key][k] = deepClone(e)
        })
      } else {
        // 如果是对象 递归调用
        newObj[key] = deepClone(obj[key])
      }
    }
  }
  return newObj
}

const copyObj = deepClone(obj)
obj.c.d = false
console.log(copyObj.c.d)
// true

这样我们实现了深拷贝
但是这样有一个问题是遇到循环引用会死循环

比如下边这个例子

const aa = {}
aa.bb = obj
obj.aa = aa

此时obj对象中的aa属性地址指向obj本身、这样进行深拷贝就会进入死循环

解决这个问题的方案其实很简单 只需要把经过转换的属性存起来 每次递归的时候都去读取一下看看是否被转换 如果被转换则直接返回不需要在尽心转换

这里我们使用map去存以转换的属性

// 增加一个hash表存储了元对象和拷贝对象的关系
function deepClone(obj, hash = new Map()) {
  if (typeof obj !== 'object' || obj === null) {
    return obj
  }
  // 拷贝前先去hash表中读取一下是否已存在 如果存在则直接返回hash表中的数据 避免进入死循环
  if (hash.get(obj)) return hash.get(obj)
  let newObj = {}
  // 每次递归都去存储对应关系
  hash.set(obj, newObj)
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      if (obj[key] instanceof Array) {
        newObj[key] = []
        obj[key].forEach((e, k) => {
          // 递归调用是将hash传入
          newObj[key][k] = deepClone(e, hash)
        })
      } else {
        // 递归调用是将hash传入
        newObj[key] = deepClone(obj[key], hash)
      }
    }
  }
  return newObj
}

这样运行一下不会报错了