js实现非递归深拷贝

360 阅读1分钟
// 深拷贝注意点:
// 1. 避免递归爆栈,使用队列进行遍历
// 2. 对尽可能多的引用类型进行拷贝,注意某些引用类型不可遍历
// 3. 避免循环引用

const CAN_NOT_TARVERSE = [
  '[object Boolean]',
  '[object Number]',
  '[object String]',
  '[object Date]',
  '[object Function]',
  '[object RegExp]',
  '[object Symbol]'
]

function getType(target) {
  return Object.prototype.toString.call(target)
}

function isReference(target) {
  return target !== null && (typeof target === 'object' || typeof target === 'function')
}

function isArray(target) {
  return isReference(target) && getType(target) === '[object Array]'
}

function isObject(target) {
  return isReference(target) && getType(target) === '[object Object]'
}

function isMap(target) {
  return isReference(target) && getType(target) === '[object Map]'
}

function isSet(target) {
  return isReference(target) && getType(target) === '[object Set]'
}

function getInitialReference(target) {
  return new target.constructor()
}

function getClonedReference(map, originReference) {
  let clonedReference
  let isCycle = false
  if (map.get(originReference)) {
    clonedReference = map.get(originReference)
    isCycle = true
  } else {
    clonedReference = CAN_NOT_TARVERSE.includes(getType(originReference))
      ? getCantTraverseReference(originReference, getType(originReference))
      : getInitialReference(originReference)
    map.set(originReference, clonedReference)
  }
  return {
    clonedReference,
    isCycle
  }
}

function getCantTraverseReference(target, type) {
  function cloneRegExp(regexp) {
    const reFlags = /\w*$/
    const result = new regexp.constructor(regexp.source, reFlags.exec(regexp))
    result.lastIndex = regexp.lastIndex
    return result
  }
  switch (type) {
    case '[object Boolean]':
    case '[object Number]':
    case '[object String]':
    case '[object Date]':
      return new target.constructor(target)
    case '[object Function]':
      return target
    case '[object RegExp]':
      return cloneRegExp(target)
    case '[object Symbol]':
      return Object(Symbol.prototype.valueOf.call(target))
    default:
      return null
  }
}

function cloneDeep(target) {
  if (!isReference(target)) return target

  const map = new Map()
  const { clonedReference: rootReference } = getClonedReference(map, target)
  const queue = [{ source: target, draft: rootReference }]

  while (queue.length) {
    const current = queue.shift()
    const { source, draft } = current
    const sourceType = getType(source)
    if (isArray(source)) {
      for (const item of source) {
        if (!isReference(item)) {
          draft.push(item)
        } else {
          const { isCycle, clonedReference } = getClonedReference(map, item)
          draft.push(clonedReference)
          !isCycle && queue.push({ source: item, draft: clonedReference })
        }
      }
    } else if (isObject(source)) {
      for (const [key, value] of Object.entries(source)) {
        if (!isReference(value)) {
          draft[key] = value
        } else {
          const { isCycle, clonedReference } = getClonedReference(map, value)
          draft[key] = clonedReference
          !isCycle && queue.push({ source: value, draft: clonedReference })
        }
      }
    } else if (isMap(source)) {
      for (const [key, value] of Object.entries(source)) {
        if (!isReference(value)) {
          draft.set(key, value)
        } else {
          const { isCycle, clonedReference } = getClonedReference(map, value)
          draft.set(key, clonedReference)
          !isCycle && queue.push({ source: value, draft: clonedReference })
        }
      }
    } else if (isSet(source)) {
      for (const value of source) {
        if (!isReference(value)) {
          draft.add(value)
        } else {
          const { isCycle, clonedReference } = getClonedReference(map, value)
          draft.add(clonedReference)
          !isCycle && queue.push({ source: value, draft: clonedReference })
        }
      }
    }
  }

  return rootReference
}