【JavaScript】实现一个深拷贝

125 阅读3分钟

本文实现了对常用数据类型的深拷贝,考虑到的情况有:

  1. key为Symbol
  2. value为Object/Array/Date/Function/RegExp/Set/Map/WeakSet/WeakMap的等情况;
  3. 循环引用。

基本思路

如果原始值为基本数据类型或函数等没必要深拷贝的,直接使用原始值;如果原始值是引用类型,则创建一个新的引用类型,然后往里面添加原始值的key:value,再使用这个新值,当然key和value需要看情况处理。

对于情况1,通过调用Object.getOwnPropertySymbols(value)获取所有Symbol键,然后添加到新的对象上;对于情况2,通过判断原始值的构造函数的名称,来new一个新的实例;对于情况3,声明了一个集合(refRecords)来记录已存在的引用类型原始值,在给新的实例赋值时,先判断原始值是否在refRecords中,在的话直接使用原始值,不在才调用deepClone,最后清空refRecords

代码

/**
 * @param {*} originValue
 * @returns {*}
 */

function deepClone(originValue) {
  // 引用记录
  const refRecords = new Set()

  // 判断是否为对象
  function isObject(_originValue) {
    const type = typeof _originValue
    return _originValue !== null && (type === 'object' || type === 'function')
  }

  // 根据原始值的数据类型深拷贝一份新值
  function createNewValue(_originValue) {
    const pName = Object.getPrototypeOf(_originValue).constructor.name
    let newValue

    if (pName === 'Object') {
      newValue = {}
      // 遍历value的自身属性
      for (const key of Object.getOwnPropertyNames(_originValue)) {
        newValue[key] = returnNewValue(_originValue[key])
        // newValue[key] = _originValue[key] // 循环引用时直接返回原对象, 避免循环引用
      }
      // 处理key为 Symbol
      for (const symbolKey of Object.getOwnPropertySymbols(_originValue)) {
        // 没必要为 Symbol 键创建新值
        newValue[symbolKey] = returnNewValue(_originValue[symbolKey])
        // newValue[symbolKey] = _originValue[symbolKey] // 循环引用时直接返回原对象, 避免循环引用
      }
    } else if (pName === 'Array') {
      newValue = []
      for (const key in _originValue) {
        newValue[key] = returnNewValue(_originValue[key])
      }
    } else if (pName === 'Date') {
      newValue = new Date(_originValue)
    } else if (pName === 'Function') {
      newValue = _originValue // 函数本来就是复用的, 因此没必要再重新拷贝一个
    } else if (pName = 'RegExp') {
      newValue = new RegExp(_originValue)
    }
      else if (pName === 'Set') {
      newValue = new Set()
      // Set 本身为可迭代对象
      _originValue.forEach((value1, value2) => {
        newValue.add(returnNewValue(value1))
      })
    } else if (pName === 'Map') {
      newValue = new Map()
      // Map 本身为可迭代对象
      _originValue.forEach((_originValue, key) => {
        newValue.set(returnNewValue(key), returnNewValue(_originValue))
      })
    } else if (pName === 'WeakSet') {
      newValue = _originValue
    } else if (pName === 'WeakMap') {
      newValue = _originValue
    }
    return newValue
  }

  // 实现深拷贝
  function _deepClone(_originValue) {
    // 原始值为 Symbol时, 返回一个新 Symbol 值
    if (typeof _originValue === 'symbol') {
      return Symbol(_originValue.description)
    }
    // 不是对象类型直接返回
    if (!isObject(_originValue)) return _originValue
    refRecords.add(_originValue)

    return createNewValue(_originValue)
  }

  // 处理循环引用问题
  function returnNewValue(_obj) {
    // 判断引用记录里面是否已存在 _obj 对象, 若存在,说明 _obj 对象被引用过, 直接用 _obj 赋值;否则递归调用
    return refRecords.has(_obj) ? _obj : _deepClone(_obj)
  }

  const newValue = _deepClone(originValue)

  // 清空引用记录
  refRecords.clear()

  return newValue
}
/* ----- 测试 ----- */
let obj = {
  name: 'fingertips',
  likes: ['apple', 'banana', 'canon', 'dog', 'emit'],
  info: {
    weight: '190kg',
    height: '999m',
    other: {
      address: 'Guangdong',
      contact: {
        qq: '888888',
        weiChat: '99999',
        email: 'xxx@qq.com'
      }
    }
  },
  date: new Date(),
  regEXP: /abc34/g,
  sym: {
    s: Symbol(909090)
  },
  foo() {
    return (ooooo += 1)
  },
  [Symbol('1234567')]: 12345,
  [Symbol.iterator]() {
    let i = -1
    return {
      next: () => {
        i++
        return {
          done: i >= this.likes.length,
          value: this.likes[i]
        }
      }
    }
  },
  [BigInt(9999999)]: 1234n, // 会被转换为字符串
  map: new Map([
    [{ map: 'map' }, 3],
    [987, 896]
  ]),
  weakMap: new WeakMap([
    [{}, 8],
    [new Date(), 'timer']
  ]),
  set: new Set([1, 2, { name: 'finger' }]),
  weakSet: new WeakSet([{}, { name: 'fingertips' }])
}

let o1 = deepClone(obj)
let o2 = deepClone(obj)
console.log(o1, o1 === o2)

// 测试循环引用
obj.info['obj'] = obj
obj.map.set(obj.map, obj.map)
obj.set.add(obj.set)
console.log(obj)