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.