collection/deepClone.ts

/** @module Collection */

import { isFunc } from '@method/isFunc'
import { cloneFunc } from '@method/cloneFunc'
import { isArr } from '@array/isArr'

/**
 * Recursively clones an object or array.
 * @example
 * const test = { foo: [ { bar: 'baz' } ] }
 * const clone = deepClone(test)
 * console.log(test === clone)) // prints false
 * console.log(test.foo === clone.foo) // prints false
 * @example
 * // Works with array too
 * deepClone([ [ [ 0 ] ] ])
 * // Returns copy of the passed in collection item
 * @function
 * @param {Object} obj - Object to clone
 * @return {Object} - Cloned Object
 */
export const deepClone = <T = Record<any, any> | any[]>(
  obj: any,
  hash: any = new WeakMap()
): T => {
  if (Object(obj) !== obj) return obj
  if (obj instanceof Set) return new Set(obj) as T
  if (hash.has(obj)) return hash.get(obj)
  if (isArr(obj)) return obj.map(x => deepClone(x)) as T
  if (isFunc(obj)) return cloneFunc(obj) as T

  const result =
    obj instanceof Date
      ? new Date(obj)
      : obj instanceof RegExp
      ? new RegExp(obj.source, obj.flags)
      : !obj.constructor
      ? Object.create(null)
      : null

  // if result is null, object has a constructor and wasn't an instance of Date nor RegExp
  if (result === null) return cloneObjWithPrototypeAndProperties(obj)

  hash.set(obj, result)

  if (obj instanceof Map)
    return Array.from(obj, ([key, val]) =>
      result.set(key, deepClone(val, hash))
    ) as T

  return Object.assign(
    result,
    ...Object.keys(obj).map(key => ({ [key]: deepClone(obj[key], hash) }))
  ) as T
}

/**
 * Helper for deepClone. Deeply clones the object, including its properties, and preserves the prototype and isFrozen and isSealed state
 * @function
 * @ignore
 * @param {Object} objectWithPrototype - any object that has a prototype
 * @returns {Object} the cloned object
 */
export const cloneObjWithPrototypeAndProperties = <
  T = Record<any, any> | any[]
>(
  objectWithPrototype: any
): T => {
  if (!objectWithPrototype) return objectWithPrototype

  const prototype = Object.getPrototypeOf(objectWithPrototype)
  const sourceDescriptors =
    Object.getOwnPropertyDescriptors(objectWithPrototype)

  for (const [key, descriptor] of Object.entries(sourceDescriptors)) {
    descriptor.value &&
      (sourceDescriptors[key].value = deepClone(descriptor.value))
  }

  const clone = Object.create(prototype, sourceDescriptors)

  if (Object.isFrozen(objectWithPrototype)) Object.freeze(clone)
  if (Object.isSealed(objectWithPrototype)) Object.seal(clone)

  return clone
}