实现深拷贝还在用JSON.parse(JSON.stringify(obj))?带你用JS实现一个完整版深拷贝函数

196 阅读6分钟

1.JSON序列化实现深拷贝

在JS中,想要对某一个对象(引用类型)进行一次简单的深拷贝,可以使用JSON提供给我们的两个方法。

  • JSON.stringfy():可以将JavaScript类型转成对应的JSON字符串;
  • JSON.parse():可以解析JSON,将其转回对应的JavaScript类型;

具体深拷贝的实现:

const obj = {
  name: 'curry',
  age: 30,
  friends: ['kobe', 'klay'],
  playBall() {
    console.log('Curry is playing basketball.')
  }
}

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

打印结果:

image-20220410161824588.png

JSON序列化实现深拷贝的优缺点:

  • 如果只是对一个简单对象进行深拷贝,那么使用该方法是很方便的;
  • 但根据上面的打印结果可以发现,原obj的方法属性并没有被拷贝到newObj中;
  • JSON序列化只能对普通对象进行深拷贝,如果对象中包含函数、undefined、Symbol等类型的值是无能为力的,会直接将其忽略掉;

2.自定义深拷贝函数

既然上面的方法不能满足我们的需求,那么就自己来一步步实现一个深拷贝函数吧。

2.1.基本功能实现

  • 实现深拷贝基本功能,暂时先不对特殊类型进行处理;
  • 定义一个辅助函数isObject,用于判断传入数据是否是对象类型;
function isObject(value) {
  const valueType = typeof value
  // 值不能为null,并且为对象或者函数类型
  return (value !== null) && (valueType === 'object' || valueType === 'function')
}
function deepClone(originValue) {
  // 判断传入的是否是对象类型,如果不是,说明是普通类型的值,直接返回即可
  if (!isObject(originValue)) {
    return originValue
  }

  const newObj = {} // 定义一个空对象
  // 循环遍历对象,取出key和值存放到空对象中
  // 注意:for...in遍历对象会将其继承的属性也遍历出来,所以需要加hasOwnProperty进行判断是否是自身的属性
  for (const key in originValue) {
    if (originValue.hasOwnProperty(key)) {
      // 递归调用deepClone,如果对象属性值中还包含对象,就会再次进行拷贝处理
      newObj[key] = deepClone(originValue[key])
    }
  }

  // 深拷贝完成,将得到新对象返回
  return newObj
}

简单测试一下:

const obj = {
  name: 'curry',
  age: 30,
  friends: {
    name: 'klay',
    age: 11
  }
}

const newObj = deepClone(obj)
console.log(newObj)
console.log(newObj.friends === obj.friends)

打印结果:

image-20220410164633590.png

2.2.其他类型处理

  • 对其它数据类型进行处理,如数组、函数、Symbol、Set、Map等;
  • 对函数类型的判断,直接返回该函数即可,因为函数本身就是可以复用的;
  • Symbol不仅可以作为value,还可以作为key,需要对key为Symbol类型的情况进行处理;
function deepClone(originValue) {
  // 1.判断传入的是否是一个函数类型
  if (typeof originValue === 'function') {
    // 将函数直接返回即可
    return originValue
  }

  // 2.判断传入的是否是一个Map类型
  if (originValue instanceof Map) {
    return new Map([...originValue])
  }

  // 3.判断传入的是否是一个Set类型
  if (originValue instanceof Set) {
    return new Set([...originValue])
  }

  // 4.判断传入的值是否是一个Symbol类型
  if (typeof originValue === 'symbol') {
    // 返回一个新的Symbol,并且将其描述传递过去
    return Symbol(originValue.description)
  }

  // 5.判断传入的值是否是一个undefined
  if (typeof originValue === 'undefined') {
    return undefined
  }

  // 6.判断传入的是否是对象类型,如果不是,说明是普通类型的值,直接返回即可
  if (!isObject(originValue)) {
    return originValue
  }

  // 7.定义一个变量,如果传入的是数组就定义为一个数组
  const newValue = Array.isArray(originValue) ? [] : {}

  // 8.循环遍历,如果是对象,就取出key和值存放到空对象中,如果是数组,就去除下标和元素放到空数组中
  // 注意:for...in遍历对象会将其继承的属性也遍历出来,所以需要加hasOwnProperty进行判断是否是自身的属性
  for (const key in originValue) {
    if (originValue.hasOwnProperty(key)) {
      // 递归调用deepClone,如果对象属性值中还包含对象,就会再次进行拷贝处理
      newValue[key] = deepClone(originValue[key])
    }
  }

  // 9.对key为Symbol类型的情况进行处理
  // 拿到所有为Symbol类型的key
  const symbolKeys = Object.getOwnPropertySymbols(originValue)
  // for...of遍历取出所有的key,存放到新对象中
  for (const sKey of symbolKeys) {
    newValue[sKey] = deepClone(originValue[sKey])
  }

  // 10.深拷贝完成,将得到新对象返回
  return newValue
}

简单测试一下:

const s1 = Symbol('aaa')
const s2 = Symbol('bbb')

const obj = {
  name: 'curry',
  age: undefined,
  friends: {
    name: 'klay',
    age: 11
  },
  hobbies: ['篮球', '足球', '高尔夫'],
  map: new Map([[1, 'aaa'], [2, 'bbb'], [3, 'ccc']]),
  set: new Set([1, 2, 3]),
  s: s1,
  [s2]: 'abc'
}

const newObj = deepClone(obj)
console.log(newObj)

打印结果:

image-20220410171306905.png

2.3.循环引用处理

我们自定义深拷贝的函数是通过递归来实现的,如果对象中有一个属性值指向了自己,那么在进行深拷贝时会陷入无限循环,这种情况也就是循环引用。

如果没有处理循环引用,那么就会不断递归,最终报错栈溢出:

image-20220410174240682.png

  • 循环引用的处理,只需要拿到新创建的对象返回即可,所以必须将这个新对象保存下来,在遇到循环引用属性时,直接就可以拿到;
  • Map和WeakMap都可以实现对对象进行存储,这里使用WeakMap进行存储,原因是WeakMap对对象的引用是弱引用;
  • 只需要将原对象作为WeakMap中的key,其值对应存放我们新创建出来的对象即可,下一次递归时进行判断WeakMap中是否存有该对象,如果有就取出返回;
function deepClone(originValue, wMap = new WeakMap()) {
  // 1.判断传入的是否是一个函数类型
  if (typeof originValue === 'function') {
    // 将函数直接返回即可
    return originValue
  }

  // 2.判断传入的是否是一个Map类型
  if (originValue instanceof Map) {
    return new Map([...originValue])
  }

  // 3.判断传入的是否是一个Set类型
  if (originValue instanceof Set) {
    return new Set([...originValue])
  }

  // 4.判断传入的值是否是一个Symbol类型
  if (typeof originValue === 'symbol') {
    // 返回一个新的Symbol,并且将其描述传递过去
    return Symbol(originValue.description)
  }

  // 5.判断传入的值是否是一个undefined
  if (typeof originValue === 'undefined') {
    return undefined
  }

  // 6.判断传入的是否是对象类型,如果不是,说明是普通类型的值,直接返回即可
  if (!isObject(originValue)) {
    return originValue
  }

  // 循环引用处理:判断wMap中是否存在原对象,如果存在就取出原对象对应的新对象返回
  if (wMap.has(originValue)) {
    return wMap.get(originValue)
  }

  // 7.定义一个变量,如果传入的是数组就定义为一个数组
  const newValue = Array.isArray(originValue) ? [] : {}

  // 循环引用处理:将原对象作为key,新对象作为value,存入wMap中
  wMap.set(originValue, newValue)

  // 8.循环遍历,如果是对象,就取出key和值存放到空对象中,如果是数组,就去除下标和元素放到空数组中
  // 注意:for...in遍历对象会将其继承的属性也遍历出来,所以需要加hasOwnProperty进行判断是否是自身的属性
  for (const key in originValue) {
    if (originValue.hasOwnProperty(key)) {
      // 递归调用deepClone,如果对象属性值中还包含对象,就会再次进行拷贝处理
      newValue[key] = deepClone(originValue[key], wMap)
    }
  }

  // 9.对key为Symbol类型的情况进行处理
  // 拿到所有为Symbol类型的key
  const symbolKeys = Object.getOwnPropertySymbols(originValue)
  // for...of遍历取出所有的key,存放到新对象中
  for (const sKey of symbolKeys) {
    newValue[sKey] = deepClone(originValue[sKey], wMap)
  }

  // 10.深拷贝完成,将得到新对象返回
  return newValue
}

简单测试一下:

const s1 = Symbol('aaa')
const s2 = Symbol('bbb')

const obj = {
  name: 'curry',
  age: undefined,
  friends: {
    name: 'klay',
    age: 11
  },
  hobbies: ['篮球', '足球', '高尔夫'],
  map: new Map([[1, 'aaa'], [2, 'bbb'], [3, 'ccc']]),
  set: new Set([1, 2, 3]),
  s: s1,
  [s2]: 'abc'
}
// 循环引用
obj.self = obj

const newObj = deepClone(obj)
console.log(newObj)
console.log(newObj.self.self.self.self)

打印结果:

image-20220410174901182.png