"Understanding Source Series 4" How lodash Realizes Deep Copies

Posted by surfer on Thu, 18 Jul 2019 04:40:02 +0200

Preface

Next to the last article How lodash achieves deep copy (Part I) Today, I'll continue to read the source code of cloneDeep to see how lodash handles deep copy of objects, functions, circular references, and so on.

Source Code Implementation of baseClone

Let's review its source code and some key annotations

function baseClone(value, bitmask, customizer, key, object, stack) {
  let result
  // Divide and judge entrance according to bit mask
  const isDeep = bitmask & CLONE_DEEP_FLAG
  const isFlat = bitmask & CLONE_FLAT_FLAG
  const isFull = bitmask & CLONE_SYMBOLS_FLAG

  // Custom clone method for. cloneWith
  if (customizer) {
    result = object ? customizer(value, key, object, stack) : customizer(value)
  }
  if (result !== undefined) {
    return result
  }

  // Filter out the original type and return directly
  if (!isObject(value)) {
    return value
  }
  
  const isArr = Array.isArray(value)
  const tag = getTag(value)
  if (isArr) {
    // Processing arrays
    result = initCloneArray(value)
    if (!isDeep) {
      // Shallow copy array
      return copyArray(value, result)
    }
  } else {
    // Processing objects
    const isFunc = typeof value == 'function'
    
    if (isBuffer(value)) {
      return cloneBuffer(value, isDeep)
    }
    if (tag == objectTag || tag == argsTag || (isFunc && !object)) {
      result = (isFlat || isFunc) ? {} : initCloneObject(value)
      if (!isDeep) {
        return isFlat
          ? copySymbolsIn(value, copyObject(value, keysIn(value), result))
          : copySymbols(value, Object.assign(result, value))
      }
    } else {
      if (isFunc || !cloneableTags[tag]) {
        return object ? value : {}
      }
      result = initCloneByTag(value, tag, isDeep)
    }
  }
  // Processing circular references with stacks
  stack || (stack = new Stack)
  const stacked = stack.get(value)
  if (stacked) {
    return stacked
  }
  stack.set(value, result)

  // Processing Map s
  if (tag == mapTag) {
    value.forEach((subValue, key) => {
      result.set(key, baseClone(subValue, bitmask, customizer, key, value, stack))
    })
    return result
  }

  // Processing Set s
  if (tag == setTag) {
    value.forEach((subValue) => {
      result.add(baseClone(subValue, bitmask, customizer, subValue, value, stack))
    })
    return result
  }

  // Processing typedArray
  if (isTypedArray(value)) {
    return result
  }

  const keysFunc = isFull
    ? (isFlat ? getAllKeysIn : getAllKeys)
    : (isFlat ? keysIn : keys)

  const props = isArr ? undefined : keysFunc(value)

  // Ergodic assignment
  arrayEach(props || value, (subValue, key) => {
    if (props) {
      key = subValue
      subValue = value[key]
    }
    // Recursively populate clone (susceptible to call stack limits).
    assignValue(result, key, baseClone(subValue, bitmask, customizer, key, value, stack))
  })

  return result
}

Processing Objects and Functions

Some of the main judgment entries have been annotated.

const isArr = Array.isArray(value)
const tag = getTag(value)

if (isArr) {
    ... // Processing of the array just now
} else {
    // Start processing objects
    // Object is the mark bit of function
    const isFunc = typeof value == 'function'
    
    // Processing Buffer (Buffer) Objects
    if (isBuffer(value)) {
        return cloneBuffer(value, isDeep)
    }
    
    // If tag is'[object Object]'
    // Or tag is'[object Arguments]'
    // Or a function without a parent object (object is passed in by baseClone, the parent of value)
    if (tag == objectTag || tag == argsTag || (isFunc && !object)) {
        // Initialize result
        // If it is a prototype chain or function, set it to an empty object
        // Otherwise, open a new object and copy the key-value pairs of the source object in turn.
        result = (isFlat || isFunc) ? {} : initCloneObject(value)
        if (!isDeep) {
            // A shallow copy of the entry object
            return isFlat
            // If it's a prototype chain, it needs to copy itself and inherited symbols
            ? copySymbolsIn(value, copyObject(value, keysIn(value), result))
            // Otherwise, just copy your own symbols
            : copySymbols(value, Object.assign(result, value))
        }
    } else {
        // When it's a function or not an error type or a weak map type
        if (isFunc || !cloneableTags[tag]) {
            return object ? value : {}
        }
        // Initialize the remaining types in the cloneableTags object as needed
        result = initCloneByTag(value, tag, isDeep)
    }
}

Among them, isBuffer handles the copy of the Buffer class, which is a concept in Node.js. It is used to create a special buffer for binary data, which allows Node.js to process binary data.

Outside baseClone, an object cloneableTags is defined, in which only error and weak map types return false, so cloneableTags[tag] means that it is not an error or weak map type.

Next, let's see how to initialize a new Object object.

function initCloneObject(object) {
    return (typeof object.constructor == 'function' && !isPrototype(object))
        ? Object.create(Object.getPrototypeOf(object))
        : {}
}

// ./isPrototype.js
const objectProto = Object.prototype
// Used to check if you are on your prototype chain
function isPrototype(value) {
    const Ctor = value && value.constructor
    // If value is a function, the prototype object of the function is taken out.
    // Otherwise, take out the prototype object of the object
    const proto = (typeof Ctor == 'function' && Ctor.prototype) || objectProto

    return value === proto
}

Among them, the judgement of typeof object. constructor =='function'is to determine that the type of value is an object or an array.

Then use Object.create to generate new objects. The Object.create() method is used to create a new object, using existing objects to provide the _proto_ of the newly created object.

object.constructor is equivalent to new Object(), and Object's constructor is a function object.

const obj = new Object();

console.log(typeof obj.constructor);
// 'function'

The prototype of an object can be obtained through Object.getPrototypeOf(obj), which corresponds to the _proto_ used in the past.

The initCloneByTag method handles the remaining multiple types of copies, including original types, such as dateTag, dataViewTag, float32Tag, int16Tag, mapTag, setTag, regexpTag, and so on.

The cloneTypedArray method is used to copy an array of types. Type array is an array-like object, which consists of Array Buffer, TypedArray and DataView. Through these objects, JavaScript can access binary data.

Circular reference

// If a stack is passed in as a parameter, use the stack in the parameter
// Otherwise a new Stack
stack || (stack = new Stack)
const stacked = stack.get(value)
if (stacked) {
    return stacked
}
stack.set(value, result)

and The Meaning, Difference and Realization of Shallow Copy and Deep Copy of "Front-end Interview Question Series 9" The cloneForce solution mentioned at the end is similar in that it uses stacks to solve the problem of circular references.

If stacked has a value, it indicates that it already exists on the stack, otherwise value and result are put on the stack. set method in Stack:

constructor(entries) {
    const data = this.__data__ = new ListCache(entries)
    this.size = data.size
}

set(key, value) {
    let data = this.__data__
    // Does data exist in the constructor of ListCache
    if (data instanceof ListCache) {
        const pairs = data.__data__
        // LARGE_ARRAY_SIZE is 2000
        if (pairs.length < LARGE_ARRAY_SIZE - 1) {
            pairs.push([key, value])
            this.size = ++data.size
            return this
        }
        // Over 200, reset data
        data = this.__data__ = new MapCache(pairs)
    }
    // If data is not in the constructor of ListCache, set operations are performed directly.
    data.set(key, value)
    this.size = data.size
    return this
}

Map and Set

These two types of deep copies take advantage of the idea of recursion, but there are differences in the way elements are added. Map uses set and Set uses add.

if (tag == mapTag) {
    value.forEach((subValue, key) => {
      result.set(key, baseClone(subValue, bitmask, customizer, key, value, stack))
    })
    return result
  }

  if (tag == setTag) {
    value.forEach((subValue) => {
      result.add(baseClone(subValue, bitmask, customizer, subValue, value, stack))
    })
    return result
}

Symbol and Prototype Chain

// Get array keys
const keysFunc = isFull
    ? (isFlat ? getAllKeysIn : getAllKeys)
    : (isFlat ? keysIn : keys)

const props = isArr ? undefined : keysFunc(value)
arrayEach(props || value, (subValue, key) => {
    // If props have a value, replace key and subValue
    if (props) {
        key = subValue
        subValue = value[key]
    }
    // Recursive cloning (vulnerable to call stack constraints)
    assignValue(result, key, baseClone(subValue, bitmask, customizer, key, value, stack))
})

return result

// ./getAllKeysIn
// Returns an array containing the attribute names on its own and prototype chains as well as Symbol
function getAllKeysIn(object) {
    const result = []
    for (const key in object) {
        result.push(key)
    }
    if (!Array.isArray(object)) {
        result.push(...getSymbolsIn(object))
    }
    return result
}

// ./getAllKeys
// Returns an array containing itself and Symbol
function getAllKeys(object) {
    const result = keys(object)
    if (!Array.isArray(object)) {
        result.push(...getSymbols(object))
    }
    return result
}

// ./keysIn
// Returns an array of attribute names on its own and prototype chains
function keysIn(object) {
    const result = []
    for (const key in object) {
        result.push(key)
    }
    return result
}

// ./keys
// Returns an array of its own attribute names
function keys(object) {
    return isArrayLike(object)
        ? arrayLikeKeys(object)
        : Object.keys(Object(object))
}

Finally, let's look at the implementation of assignValue.

// ./assignValue
const hasOwnProperty = Object.prototype.hasOwnProperty

function assignValue(object, key, value) {
  const objValue = object[key]

  if (!(hasOwnProperty.call(object, key) && eq(objValue, value))) {
    // value is not zero or available
    if (value !== 0 || (1 / value) == (1 / objValue)) {
      baseAssignValue(object, key, value)
    }
  // value is undefined, and there is no key in the object
  } else if (value === undefined && !(key in object)) {
    baseAssignValue(object, key, value)
  }
}

// ./baseAssignValue
// Basic Implementation of Assignment
function baseAssignValue(object, key, value) {
  if (key == '__proto__') {
    Object.defineProperty(object, key, {
      'configurable': true,
      'enumerable': true,
      'value': value,
      'writable': true
    })
  } else {
    object[key] = value
  }
}

// ./eq
// Compare the two values to be equal
function eq(value, other) {
  return value === other || (value !== value && other !== other)
}

In the final eq method, the judgment value!== value & other!== other, which is written to judge NaN. Specific can refer to this article. "Understanding Source Series 2" Several Knowledge Points I Learned from lodash Source

summary

cloneDeep includes various types of deep copy methods, such as buffer in node, type array, etc. The idea of stack is used to solve the problem of circular reference. Map and Set add elements in a similar way, set and add, respectively. NaN is not equal to itself.

Source code interpretation of deep copies is over by now. The writing process of this article also spent several nights, feeling that I was really competing with myself. Just because I want to explain the implementation process of the source code as much as possible, it takes a lot of time to find the information and think comprehensively. It's good that I didn't give up in the end, and the harvest is quite abundant. Some skills learned from the source code have also been used in the actual project, which improves the performance and readability.

In recent years, due to work reasons, writing articles has been slackened, so we should continue to write them. Since then, the "Super Brother Front Stack" has been updated, and every article will be updated to the same time. Nuggets,segmentfault and github Up.

Personal time and energy are limited, there are some deficiencies in the expression. We also hope that readers can make more corrections and support, and look forward to more exchanges. Thank you.~

PS: Welcome to pay attention to my public number "Super Brother Front End Stack" and exchange more ideas and technologies.

Topics: Javascript Attribute github