ProJSLib/jsbits

View on GitHub
packages/deep-clone/index.ts

Summary

Maintainability
A
0 mins
Test Coverage
/// <reference lib="es2015" />

type CloneFn = <T> (obj: T) => T
type GetKeysFn = (obj: any, type: string) => Array<string | symbol>

const _nodejs = (function () {
  const g = typeof global === 'object' && global
  const m = g && g.process &&
    typeof g.Buffer === 'function' && /^v?(\d+)/.exec(g.process.version)
  return (m && m[1] as any) | 0
})()

const _OP = Object.prototype
const _toString = _OP.toString
const _ownKeys = typeof Reflect === 'object' &&
  typeof Reflect.ownKeys === 'function' && Reflect.ownKeys

/**
 * Cloning method of various types.
 */
const enum ObjType {
  Simple = 1,
  Array,
  ArrayBuffer,
  DataView,
  MapOrSet,
  Error,
  Arguments,
  AsIs,
}

/**
 * Clonable types with special handlers
 */
const clonable: { [k: string]: ObjType } = {
  Date: ObjType.Simple,
  RegExp: ObjType.Simple,
  String: ObjType.Simple,
  Number: ObjType.Simple,
  Boolean: ObjType.Simple,
  Float32Array: ObjType.Simple,
  Float64Array: ObjType.Simple,
  Int8Array: ObjType.Simple,
  Int16Array: ObjType.Simple,
  Int32Array: ObjType.Simple,
  Uint8Array: ObjType.Simple,
  Uint8ClampedArray: ObjType.Simple,
  Uint16Array: ObjType.Simple,
  Uint32Array: ObjType.Simple,
  Array: ObjType.Array,
  ArrayBuffer: ObjType.ArrayBuffer,
  SharedArrayBuffer: ObjType.ArrayBuffer,
  DataView: ObjType.DataView,
  Error: ObjType.Error,
  Map: ObjType.MapOrSet,
  Set: ObjType.MapOrSet,
  Arguments: ObjType.Arguments,
  Atomics: ObjType.AsIs,
  JSON: ObjType.AsIs,
  Math: ObjType.AsIs,
  Promise: ObjType.AsIs,
  WeakMap: ObjType.AsIs,
  WeakSet: ObjType.AsIs,
  XMLHttpRequest: ObjType.AsIs,
}

const arrayLike = [
  'Array',
  'String',
  'Float32Array',
  'Float64Array',
  'Int8Array',
  'Int16Array',
  'Int32Array',
  'Uint8Array',
  'Uint8ClampedArray',
  'Uint16Array',
  'Uint32Array',
]

/**
 * Creates a function to get enumerable symbol names of an object.
 *
 * The array is returned as string[] because the lack of full typings
 * for Symbol keys in TS v3
 *
 * @param {boolean} exact All keys?
 * @returns {Function} Extractor
 * @private
 */
const getKeyGetter = function (exact: boolean) {

  // istanbul ignore next
  const _keys = exact ? Object.getOwnPropertyNames : Object.keys
  const _getSymbols = Object.getOwnPropertySymbols

  // istanbul ignore next: until we can test IE11
  if (!_getSymbols) {
    return _keys
  }

  // All the keys, including the non-enumerable ones
  // istanbul ignore if: until we can test IE11
  if (exact) {
    return (obj: any) => _keys(obj).concat(_getSymbols(obj) as any)
  }

  // Only enumerable keys and symbols
  const _isEnum = _OP.propertyIsEnumerable
  return (obj: any) => {
    const objkeys = _keys(obj) as any[]
    const symbols = _getSymbols(obj)

    for (let i = 0; i < symbols.length; i++) {
      if (_isEnum.call(obj, symbols[i])) {
        objkeys.push(symbols[i])
      }
    }

    return objkeys as string[]
  }
}

/**
 * Creates a function to get the properties and symbol names of an object.
 *
 * @param {boolean} exact Include non-enum keys?
 * @returns {Function} Extractor
 * @private
 */
const getKeysFac = function (exact: boolean) {

  // Avoid creating multiple anonymous functions
  const _filtIdx = (prop: any) => prop !== '0' && (prop | 0) <= 0 && prop !== 'length'
  const _getKeys = exact && _ownKeys || getKeyGetter(exact)

  return (obj: {}, type: string) => {
    const objkeys = _getKeys(obj) as Array<string | symbol>
    return ~arrayLike.indexOf(type) ? objkeys.filter(_filtIdx) : objkeys
  }
}

/**
 * Clone the Map or Set and copy its elements.
 *
 * @param {Map|Set} src Object to copy
 * @param {Function} fn Cloner
 * @param {string} type Type of object
 * @returns {Map|Set} The clone
 * @private
 */
const cloneMapOrSet = <T=any> (src: Map<T, T> | Set<T>, fn: CloneFn, type?: string) => {
  const dest = new (src as any).constructor()

  // IE11 will not copy items, anyway, we need clone keys and values
  const cb = type === 'Set'
    ? function (this: Set<T>, v: T) { this.add(fn(v)) }
    : function (this: Map<T, T>, v: T, k: T) { this.set(fn(k), fn(v)) }

  ;(src as Set<T>).forEach(cb, dest)
  return dest
}

/**
 * Clone the given DataView object.
 *
 * @param {DataView} src Object to clone
 * @returns {DataView} Clone
 * @private
 */
const cloneDataView = (src: DataView) => {
  const buffer = src.buffer.slice(0)
  return new (src as any).constructor(buffer, src.byteOffset, src.byteLength)
}

const cloneError = (src: Error) => {
  const err = new (src as any).constructor(src.message)
  return Object.defineProperty(err, 'stack', {
    value: src.stack, configurable: true, writable: true,
  })
}

/**
 * Copies arguments to an object without prototype - Adds the length property.
 *
 * @param {Arguments} src arguments pseudo-array
 * @returns {object} -
 * @private
 */
const cloneArguments = (src: any[]) => {
  const args = Object.create(null)
  return Object.defineProperty(args, 'length', {
    value: src.length, configurable: true, writable: true,
  })
}

// Faster array cloning
const cloneArray = (src: any, fn: CloneFn) => src.map(fn)

const cloneFn: { [k: number]: (obj: any, fn: CloneFn, _?: string) => any } = {
  [ObjType.AsIs]: (obj: any) => obj,
  [ObjType.Arguments]: cloneArguments,
  [ObjType.Array]: cloneArray,
  [ObjType.ArrayBuffer]: (obj: any) => obj.slice(0),
  [ObjType.DataView]: cloneDataView,
  [ObjType.Error]: cloneError,
  [ObjType.MapOrSet]: cloneMapOrSet,
}

/**
 * Creates a new object intance of the given type.
 *
 * @param {object} obj Non-null object
 * @param {string} type Object type
 * @param {Function} fn Cloner
 * @returns {object} The new instance
 * @private
 */
const createObject = (obj: any, type: string, fn: CloneFn) => {
  const cloneType = clonable[type]

  if (cloneType === ObjType.Simple) {
    return obj.slice && _nodejs && Buffer.isBuffer(obj)
      ? obj.slice(0)
      : new obj.constructor(obj.valueOf())
  }

  if (cloneFn[cloneType]) {
    return cloneFn[cloneType](obj, fn, type)
  }

  return type.lastIndexOf(' Iterator', type.length - 9) > -1
    ? obj : Object.create(Object.getPrototypeOf(obj))
}

/**
 * Get the object type, taking care about node <6 returning 'Object' for
 * Promise.
 */
const getObjectType = (function () {

  const _getTag = (obj: object) => _toString.call(obj).slice(8, -1)

  // istanbul ignore else
  if (!_nodejs || _nodejs >= 5) {
    return _getTag
  }

  // istanbul ignore next
  return (obj: object) => {
    const tag = _getTag(obj)
    return tag === 'Object' && obj.constructor && obj.constructor.name === 'Promise'
      ? 'Promise' : tag
  }
})()

/**
 * Factory to create a "clone" function for loosy or exact mode.
 *
 * @param {Function} getKeys Keys & symbols extractor
 * @returns {Function} Cloner
 * @private
 */
// codebeat:disable[ABC,BLOCK_NESTING]
const cloneFac = function (getKeys: GetKeysFn) {

  const _clone = function _clone<T> (obj: any): T { // -eslint-disable-line max-statements

    // Filter out null, undefined, NaN, primitive values, and functions
    if (!obj || typeof obj !== 'object') {
      return obj
    }

    // The type also allows optimize the getKeys function.
    const type = getObjectType(obj)

    // Get a new object of the same type and the properties of the source
    const clone = createObject(obj, type, _clone)
    const props = getKeys(obj, type)

    for (let i = 0; i < props.length; i++) {
      const prop = props[i]
      const desc = Object.getOwnPropertyDescriptor(obj, prop)!

      // NOTE: `value` must be excluded for setter/getter
      if (desc.value !== undefined) {
        desc.value = _clone((obj as any)[prop])
      }

      Object.defineProperty(clone, prop, desc)
    }

    return clone
  }

  return _clone
}
// codebeat:enable[ABC,BLOCK_NESTING]


/**
 * Deep clone of enumerable properties.
 *
 * @param {object} obj Any object
 * @returns {object} The clone.
 * @private
 */
const looseClone = cloneFac(getKeysFac(false))

/**
 * Deep clone of all the properties, including the non-enumerable ones.
 *
 * @param {object} obj Any object or value
 * @returns {object} The clone.
 * @private
 */
const exactClone = cloneFac(getKeysFac(true))

/**
 * Performs a deep cloning of an object own properties and symbols, preserving
 * its prototype.
 *
 * By default `cloneObject` works in "loosy mode", where it clones only
 * the object _enumerable_ properties and symbols.
 *
 * To enable the "exact mode" and clone all, pass `true` in the second parameter.
 *
 * Both modes retain all the attributes of the copied properties (enumerable,
 * configurable, writable) and correctly transfer the `get` and/or `set`
 * methods, although these, like the other function-type values,
 * _are copied by reference_.
 *
 * Try to limit the usage of this function to POJOs, as this function does not
 * work for objects with constructor that requires parameters (other than
 * the most JS built-in Objects), nor objects with recursive references.
 *
 * @template T
 * @param {T} value Value to clone, mostly an object or array.
 * @param {boolean} [exact=false] If `true`, also clone the non-enumerable properties
 * @returns {T} The cloned object or value.
 * @since 1.0.0
 */
const deepClone = function _deepClone<T> (value: T, exact?: boolean): T {
  return exact ? exactClone(value) : looseClone(value)
}

// Export here so that TS places the JSDdoc in the correct position.
export = deepClone