Based on LoDash’s baseClone function, this article analyzes different types of deep and light copies in JavaScript. Lodash deep and shallow copy based on JavaScript structured clone algorithm implementation, partial transformation. With this article, you’ll be able to get a fuller picture of the types in JavaScript.

Before starting, there may be some clerical errors or errors in this article, please point out in the comments section, I will correct them in time

The concept of shallow copy

First, the concept of shallow and deep copies is only relevant to composite objects.

  • Shallow copy constructs a new composite object into which references to objects from the original object are inserted.
  • The deep copy constructs a new composite object and then recursively inserts a copy of the object from the original object into it.

An object generated from a shallow copy will affect the original object when it updates an internal object.

Structured cloning algorithm

Used in Workes’ postMessage data delivery, storing data via IndexdDB, or other apis. It copies incoming objects recursively, and internally maintains a map of previously iterated property references, preventing infinite loops

Objects for which structured cloning algorithms do not work

  • Symbol
  • Function
  • Such asDOM NodesThe object of
  • Do not preserve certain object attributes
    • RegExpthelastIndexProperties (aboutlastIndexDescription of attributes) (Note about Regexp sticky )
    • Attribute descriptor,getter.setterAnd metadata-like features (metedata)
    • prototypePrototype chain

lodash baseCloneCompared with structured clone algorithm

  • supportSymbolA copy of the
  • Error objects are not supportedErrorA copy of the
  • The regular expression is preservedlastIndex
  • Prototype chain retention
  • Copying non-replicable objects does not throw an exception

The following is a reminder of some of the methods (along with some of the things I’m not familiar with myself) that don’t need to jump straight to loDash for copying different JavaScript objects

Partial method recall

String manipulation

RegExp.prototype.exec

let re = /quick\s(brown).+? (jumps)/igd;
let result = re.exec('The Quick Brown Fox Jumps Over The Lazy Dog');
// ['Quick Brown Fox Jumps', 'Brown', 'jumps']
// 0 Returns the matched string
// 1-n returns the captured subexpression
// index returns the position of the captured string in the original string
// input returns the original string
// indices A two-dimensional array containing the start index and end index of matching strings
// indices[0] [4, 25] 'Quick Brown Fox Jumps'
// indices[1] [10, 15] 'Brown'
// indices[2] [20, 25] 'Jumps'

Copy the code

Groups and indices.groups property demonstration

Indices. Groups Captures a named capture group for a regular expression, or undefined if there is no named capture group.
// Make a slight change to the above example
let re = /quick\s(? 
      
       brown).+? (? 
       
        jumps)/ig
       
      d;
let result = re.exec('The Quick Brown Fox Jumps Over The Lazy Dog');
// indices.groups.group1 [10, 15]
// indices.groups.group2 [20, 25]
// groups {group1: 'Brown', group2: 'Jumps'}
Copy the code

Re attributes are as follows:

attribute describe value
lastIndex Instance properties, not defined on stereotype connections. The location index at the start of the next match. There is onlygoryIdentifier, the property is automatically set.lastIndexIt is not automatically reset when different strings are matched. For example, string B is matched after string A is matched. In this case, the starting position of string B is the same as that of string AlastIndexlocation 25
dotAll sIdentifier..Matches any character except newline by default. addsLater,.Can match all characters. Cooperate withuUse can match allunicodecharacter false
hasIndices dIdentifier. The identifier is generatedindicesProperty that returns the values of the start and end indexes of the matched substring true
ignoreCase iIdentifier. Ignore case false
global gIdentifier. Global match true
multiline mIdentifier. Whether there is a cross-row match. After using this identifier,^and$Matches the beginning and end of each string instead of the beginning and end of the string false
source Current regular expression /quick\s(brown).+? (jumps)/igd
sticky yIdentifier. fromlastIndexThe position starts to match, matches only once, and willlastIndexMove backward. If no match is found, thelastIndexSet to zero false
unicode uIdentifier. Used to match patterns as sequences of Unicode code points. false

Use the regexp constructor in the while loop to declare the regular expression directly or without the G identifier. Could cause an infinite loop because lastIndex is initialized to 0 every time.

// 
// dotAll 
// hasIndices
Copy the code

String.prototype.replace

If Pattern is a string, only the first matching string is replaced. String as the second argument

Pattern Insert value
$$ Insert a$operator
$& Inserts the matching substring
$` Inserts the part of the string before the matching substring
$' Inserts the part of the string following the matching substring.
$n Inserts the NTH expression captured, starting with an index of 1
$<Name> Insert the value corresponding to the named capture group

e.g:

var str ="cabc"
var str1 = str.replace(/a/.'$$') // c$bc
var str2 = str.replace(/a/.'$&') // cabc
var str3 = str.replace(/a/.'$`) // ccbc
var str4 = str.replace(/a/.` $' `) // cbcbc
var str5 = str.replace(/(a)(b)/.'$1') // cac
var str6 = str.replace(/ (? 
      
       b)(? 
       
        c)/
       
      .'$<test>') // cab
Copy the code

The function takes the following arguments:

Custom name value
match Matched string
p1,p2... Returns the captured value when the first argument is a regular expression
offset The starting position of the matched string in the string
string The string to be replaced
groups Returns the named capture group and its name

String.prototype.replaceAll

When using a regular expression, a g identifier must be added

String.prototype.match

It takes a regular expression as an argument and returns [“”] without taking any arguments. Non-regular expressions are converted by calling new RegExp. There are two main cases: regular expressions containing g and regular expressions without G

  • containsgReturns an array of all matched strings
  • Does not containgReturns only the first full match and its associated capture group (similar toexecIn the tablep1,p2..andreplacethe$1, $2...). Additional attributes are also includedgroups.indexandinputSimilar to theexecmethods
  • If no match is found, null is returned. andexecThe return values are consistent.

String.prototype.matchAll

While matchAll looks like a global matching version of Match, it behaves more like exec. Like the relationship between generator and Async.

Exec requires us to manually traverse the matching array and related parameters obtained (index, input).

const regexp = new RegExp('foo[a-z]*'.'g');
const str = 'table football, foosball';
let match;

while((match = regexp.exec(str)) ! = =null) {
  console.log(`Found ${match[0]} start=${match.index} end=${regexp.lastIndex}. `);
  // expected output: "Found football start=6 end=14."
  // expected output: "Found foosball start=16 end=24."
}
Copy the code

MatchAll can be implemented directly like this:

const regexp = new RegExp('foo[a-z]*'.'g');
const str = 'table football, foosball';
const matches = str.matchAll(regexp);

for (const match of matches) {
  console.log(`Found ${match[0]} start=${match.index} end=${match.index + match[0].length}. `);
}
// expected output: "Found football start=6 end=14."
// expected output: "Found foosball start=16 end=24."

// matches iterator is exhausted after the for.. of iteration
// Call matchAll again to create a new iterator
Array.from(str.matchAll(regexp), m= > m[0]);
// Array [ "football", "foosball" ]
Copy the code

The only difference from exec is that matchAll does not modify the lastIndex attribute of regular expression instances.

Also like replaceAll, passing a regular expression without g causes matchAll to throw an exception

RegExp.prototype.test

Calling this method changes the lastIndex value of the regular expression instance and will not be reset to 0 until false is returned

String.prototype.search

An instance of a regular expression is received as an argument, or if it is not, the new RegExp(object) method is called for type conversion

The return value is the starting position of the target string, or -1 if there is no match

Property traversal method recall

for.. in

for… The IN statement iterates over all the string key enumerable properties of an object (ignoring those with the Symbol key), including inherited enumerable properties.

for… The in loop iterates over the properties of an object in any order. The reason. Not suitable for iterating over objects whose index order is important, such as arrays

Object.keys()

Returns an enumerable property of the object itself.

Iterate in the same order as a normal loop.

Object.getOwnPropertyNames()

Gets all properties of the object itself, including those that are not enumerable (except those that use Symbol)

The order of enumerable properties in an array is the same as for… The in loop (or object.keys ()) exposes Object properties in the same order. According to ES6, the object’s integer keys (enumerable and non-enumerable) are first added to the array in ascending order, and then string keys are added in insertion order.

hasOwnProperty

Determines whether a corresponding attribute exists on an object. For an array object, you can determine whether an index exists

in

The IN operator returns true if the specified property is in the specified object or its stereotype chain.

ObjectInstance method recall

Object.prototype.propertyIsEnumerable

This method determines whether an object can be for… In enumeration, excluding properties on the stereotype chain

Object.getOwnPropertySymbols()

Gets the Symbol key property on the object

Object.getOwnPropertyNames()

Gets all properties on the object (including non-enumerable properties), but not properties with Symbol as the key

Object.assign()

Copy all of the propertyIsEnumerable hasownProperties of an object (or more) to the target object. Returns the modified object.

Use [[GET]] for the source object and [[SET]] for the target object. So the corresponding getters and setters methods can be triggered

Object.getOwnPropertyDescriptor()

Returns a configuration item for the specified property on an object. Properties in JavaScript consist of string value names or symbols and property descriptors.

There are two main forms of property descriptors that exist in objects: data descriptors and accessor descriptors. A data descriptor is an attribute with a value that may or may not be writable. Accessor descriptors are properties described by a pair of getter-setter functions. The descriptor must be one of these two styles; It cannot be both.

An exception is thrown if the descriptor has both [value or writable] and [get or set] keys.

Object.defineProperty()

Object.defineProperty(obj, prop, descriptor)

By default, values added using Object.defineProperty() are both writable (false) and enumerable (false).

To ensure that the default values of property descriptors are preserved, you can pre-freeze the Object, specify all options explicitly, or point to NULL with Object.create(null).

// using __proto__
var obj = {};
var descriptor = Object.create(null); // no inherited properties
descriptor.value = 'static';

// not enumerable, not configurable, not writable as defaults
Object.defineProperty(obj, 'key', descriptor);
Copy the code

There is usually a difference between using.assignment and using Object.defineProperty().

var o = {};

o.a = 1;
// is equivalent to:
Object.defineProperty(o, 'a', {
  value: 1.writable: true.configurable: true.enumerable: true
});

// On the other hand,
Object.defineProperty(o, 'a', { value: 1 });
// is equivalent to:
Object.defineProperty(o, 'a', {
  value: 1.writable: false.configurable: false.enumerable: false
});
Copy the code

newThe operatormemories

For the following example:

function Foo(bar1, bar2) {
      this.bar1 = bar1;
      this.bar2 = bar2;
    }
var myFoo = new Foo('Bar 1'.2021);
Copy the code

New Foo and new Foo() are equal. That is, if no argument list is specified, Foo is called without arguments.

TypedArraymemories

A TypedArray object represents an array-like view of an underlying binary data buffer.

ArrayBuffermemories

An ArrayBuffer object is used to represent a generic, fixed-length buffer of raw binary data

You cannot manipulate the contents of an ArrayBuffer directly. However, you can create a typed array objects or DataView object that represents a buffer in a specific format and use it to read and write the contents of the buffer.

The ArrayBuffer() constructor creates a new ArrayBuffer of a given length, in bytes. You can also get a Buffer object from existing data, for example, from a Base64 string or from a local file.

Base64

Base64 is a set of binary-to-text encoding schemes that represent binary data in ASCII string format by converting binary data to a Base64 representation

  • btoa: used to encode binary data asASCIICode encoded data
  • atob: used for theASCIICode encoded data is decoded into binary data

The problem

  • Each Base64 digit represents exactly six bits of data (127). Thus, the three 8-bit bytes of the input string/binary (3 x 8 bits = 24 bits) can be represented by four 6-bit Base64 digits (4 x 6 = 24 bits). The volume increases after coding (133%). If the encoded data is small, the increase may be larger.

  • Unicode problem. DOMStrings is a 16-bit encoded string. The solution is as follows:

    • The first is to escape the entire string and then encode it

      function b64EncodeUnicode(str) {
        // %E2%9C%93%20%C3%A0%20la%20mode
        console.log( encodeURIComponent(str))
        //%0A - %9F -> 0x0A - 0x9F
        return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g.function(match, p1) {
            return String.fromCharCode('0x' + p1);
        }));
      }
      Copy the code
    • The second is to convert utF-16 DOMString to a UTF-8 character array and then encode it.

Data URLs

Data :[

][;base64],
If data contains characters defined in RFC 3986 as reserved Characters) [datatracker.ietf.org/doc/html/rf…]. , Spaces, newlines, or other non-print characters, which must be percent-encoding

FileReader

The FileReader object allows a Web application to asynchronously read the contents of a File (or raw data buffer) stored on the user’s computer, using a File or Blob object to specify which File or data to read.

  • FileReader.prototype.readAsDataURLOften used for front end preview
  • FileReader.prototype.readAsBinaryStringIt is used to upload files

File System Access API

This API allows you to interact with files on a user’s local device or on a network file system accessible to the user.

Blob

  • Blob.prototype.arrayBuffer()Returns aPromiseObject resolverarrayBufferobject
  • Blob.prototype.slice()Returns a new Blob object containing data in the specified byte range of the Blob that called it. Array-likeslicemethods
  • Blob.prototype.stream()Returns a ReadableStream that can be used to read the Blob content.
  • Blob.prototype.text()Returns a use ofUSVString resolvethePromiseObject that contains the entire contents of the Blob interpreted as UTF-8 text.
Using files from web applications

Some examples of uploading are given

  • You can listen in<input>thechangeEvent to get the instance Files property of FileList
  • If you need to modify it<input>Style, can be setdisplay:noneoroverflow:hidden. And then it fires on the other elements<input>theclick()The event
  • Can also not hide cooperation<label>Of the labelforProperty to implement
  • Can be achieved byURL.createObjectURL()To convert file toDataURL. Functions similar toFileReader.prototype.readAsDataURL. After the conversion is complete, it can passURL.revokeObjectURL()To free up resources
FileList

Objects of this type are returned by the files attribute of the HTML element;

DataTransfer

This object is available from the dataTransfer property for all drag events.

File

File inherits from Blob. The main attributes include:

  • File.prototype.lastModifiedLast modified
  • File.prototype.type``MIMEtype
  • File.prototype.sizeThe size of the
  • File.prototype.nameThe file name

Lodash copy-handling of different objects in JavaScript

Basic data types

An isObject function is used to determine whether this type needs special handling. The isObject function source code is as follows:

function isObject(value) {
  const type = typeof value
  For example: normal objects, functions, arrays, regular expression objects, objects generated by the String constructor, objects generated by the Number constructor, and so on
  // The definition of the Object type https://262.ecma-international.org/7.0/#sec-object-type
  returnvalue ! =null && (type === 'object' || type === 'function')}Copy the code

Return null undefined string number Boolean. For objects created by new calls to the constructor of the base datatype (excluding null and undefined), a new base datatype object is created from the object’s constructor property, which is a reference to the instance constructor. The initCloneByTag function looks like this (the rest of the function will be listed later) :

function initCloneByTag(object, tag, isDeep) {
  // Get a reference to the object constructor
  const Ctor = object.constructor
  switch (tag) {
    ...
    // Boolean to number
    case boolTag:
      return new Ctor(+object)
    ...
    case numberTag:
    case stringTag:
      return new Ctor(object)
  }
}
Copy the code

An array of

Arrays are one of the main objects handled in object copying (the other being object types). The judging process is as follows:

  • Checks whether the object is an array

    const isArr = Array.isArray(value)
    Copy the code
  • Generate an instance of an Array, that is, create a new instance (possibly some other type of Array based on an Array implementation) based on inheritance. Handling special arrays at the same time, such as exec and matchAll in the recall above, returns arrays that contain additional index and input attributes

    The initCloneArray function creates a new instance and special handling:

    function initCloneArray(array) {
      const { length } = array
      const result = new array.constructor(length)
    
      // Add properties assigned by `RegExp#exec`.
      The matchAll method also returns an array of the same type
      // Special processing of the result array returned by the regular exec. Exec returns an array with the matched string as the first entry, and the input property holds the matched string
      // index stores the position of the current matching string and updates the lastIndex property of the regular expression as the starting position of the next match
      // exec returns null if no match is found, and sets the lastIndex of the re object to 0
      / / more details: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec
      if (length && typeof array[0= = ='string' && hasOwnProperty.call(array, 'index')) {
        result.index = array.index
        result.input = array.input
      }
      return result
    }
    Copy the code

    The baseClone function holds the initialized array instance object:

    .// result is the return value of baseClone
    result = initCloneArray(value)
    Copy the code
  • Shallow copy, which iterates directly over the old array and writes the items of the old array to the new array

    CopyArray function:

    function copyArray(source, array) {
      let index = -1
      const length = source.length
    
      array || (array = new Array(length))
      while (++index < length) {
        array[index] = source[index]
      }
      return array
    }
    Copy the code

    Why not just use slice for copyArray? Because iterating returns a dense array, the Empty item will be filled with undefined. By doing this, you can enable methods such as forEach Map of arrays to operate on arrays normally

  • Deep copy, forEach each entry in the old array and recursively call baseClone to process each entry

    ArrayEach is loDash’s version of forEach for arrays.

    // Iteratee is the callback of forEach
    // the second parameter of forEach 'thisArg' is not implemented here
    // Unlike Map Reduce, forEach's callback always returns undefined
    function arrayEach(array, iteratee) {
      let index = -1
      const length = array.length
    
      while (++index < length) {
        if (iteratee(array[index], index, array) === false) {
          break}}return array
    }
    Copy the code

    Call arrayEach (equivalent to array.prototype. forEach) and baseClone recursively to operate on Array items (some of the source code has been modified here for clarity) :

    // value refers to the current array object
    arrayEach(value, (subValue, key) = > {
      // result Generates a new array instance
      // Index of the key array
      // subValue Specifies the value at the array index
      // Bitmask is used to determine the type of recursion, as shown below
      // customizer custom recursive function
      // Stack is used to record references to values that have been processed, avoiding circular references within objects
      assignValue(result, key, baseClone(subValue, bitmask, customizer, key, value, stack))
    })
    Copy the code

    AssignValue is based on the baseAssignValue function, which assigns values to an Object based on its keys and values. Similar to Object.assign, which merges only the enumerable attributes of the target Object. AssignValue is called directly because the value has been processed by baseClone, so it can be assigned directly to deep copy. BaseAssignValue specifically handles the __proto__ attribute and resets the default value of Object.defineProperty as follows:

    function baseAssignValue(object, key, value) {
      // Handle the __proto__ attribute in particular
      if (key == '__proto__') {
        Object.defineProperty(object, key, {
          'configurable': true.'enumerable': true.'value': value,
          'writable': true})}else {
        object[key] = value
      }
    }
    Copy the code

    On the basis of the baseAssignValue function, the baseAssignValue takes three parameters, object key values. Object Indicates the object to which attributes are to be written, key indicates the key to be written, and value indicates the value of key. The following additional conditions were processed:

    • objectDoes it includekeyIf so, judgeobject[key]Whether or notvalue
    • There are additional conditions for the above conditions because+ 0 = = = 0So, yes0Special treatment
    • ifvalueNot passed and the source object does not containkey, the writeundefinedOtherwise, keep the original code as follows:
    const hasOwnProperty = Object.prototype.hasOwnProperty
    function assignValue(object, key, value) {
      const objValue = object[key]
    
      // object does not contain the current key and object[key] is equal to the value passed
      // That is, the current key is in the prototype chain of object or does not contain the current key
      if(! (hasOwnProperty.call(object, key) && eq(objValue, value))) {// If value is not +0 or -0, the key is directly defined on the current object
        if(value ! = =0| | -1 / value) === (1 / objValue)) {
          baseAssignValue(object, key, value)
        }
        // undefined && key is not on the object or its prototype chain
      } else if (value === undefined && !(key in object)) {
        baseAssignValue(object, key, value)
      }
    }
    Copy the code

    Eq function to handle the case where two objects are congruent and compatible with NaN === NaN (object. is(NaN, NaN) // true) :

    function eq(value, other) {
      // Special processing NaN NaN
      returnvalue === other || (value ! == value && other ! == other) }Copy the code

Ordinary objects

  • Initializes a new instance of an object based on its prototype. This function is the initCloneObject function, which creates a new instance from the object’s prototype. Constructor () {constructor () {constructor () {constructor () {constructor () {constructor () {constructor () {constructor () {constructor (); InitCloneObject ();

    function initCloneObject(object) {
      // Object is an instance and not a prototype object
      return (typeof object.constructor === 'function' && !isPrototype(object))
        // Create a prototype object based on the object instance
        ? Object.create(Object.getPrototypeOf(object))
        : {}
    }
    Copy the code

    Object.constructor === ‘function’ is used in cases where object.create (null) is compatible. The isPrototype source code is as follows:

    const objectProto = Object.prototype
    function isPrototype(value) {
      const Ctor = value && value.constructor
      const proto = (typeof Ctor === 'function' && Ctor.prototype) || objectProto
    
      return value === proto
    }
    Copy the code
  • Shallow copy

    • Tile copy. The idea is to go through for… In access to the Object and Object prototype can be enumerated attribute on the chain, then through the Object. GetOwnPropertySymbols and, in turn, find and upward from the prototype chain through Object. The prototype. PropertyIsEnumerable filter Symbol objects cannot be enumerated. The source code is as follows:

      copySymbolsIn(value, copyObject(value, keysIn(value), result))
      Copy the code

      The keysIn function is used for enumerable properties on the object and object prototype chain. The source code is as follows:

      function keysIn(object) {
        const result = []
        for (const key in object) {
          result.push(key)
        }
        return result
      }
      
      Copy the code

      The copyObject function is used to copy an attribute from a Value object to a result (this function can actually customize the implementation of the function with a customizer argument, but it’s omitted because it’s not needed here). This function references both the baseAssignValue and assignValue functions. Note that the assignment method of baseAssignValue is directly from the value of the source object.

      function copyObject(source, props, object) {
        // Whether to create a new empty object
        constisNew = ! object object || (object = {})for (const key of props) {
          // Whether the custom copy function customizer is passed
          let newValue = source[key]
          / / the object did not pass
          if (isNew) {
            baseAssignValue(object, key, newValue)
          } else {
            assignValue(object, key, newValue)
          }
        }
        return object
      }
      Copy the code

      Having processed the ordinary enumerable properties, now work on the Symbol type properties. CopySymbolsIn essentially calls the copyObject function internally again, but instead of getting the object key from keyIn, we get it from getSymbolsIn. The source code is as follows:

      function copySymbolsIn(source, object) {
        // getSymbolsIn gets the enumerable symbol property on the prototype chain
        return copyObject(source, getSymbolsIn(source), object)
      }
      
      Copy the code

      GetSymbolsIn looks up the enumerable Symbol properties through the prototype chain and calls getSymbols to filter the non-enumerable Symbol properties. The source code is as follows:

      function getSymbolsIn(object) {
        const result = []
        while(object) { result.push(... getSymbols(object))// Object(Object) Compatible with ES5
          object = Object.getPrototypeOf(Object(object))
        }
        return result
      }
      
      Copy the code

      GetSymbols source code is as follows:

      /** Built-in value references. */
      // This method determines whether an object can be for... In enumeration, to remove properties from the stereotype chain
      // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/propertyIsEnumerable
      const propertyIsEnumerable = Object.prototype.propertyIsEnumerable
      
      /* Built-in method references for those with the same name as other `lodash` methods. */
      const nativeGetSymbols = Object.getOwnPropertySymbols
      
      /**
       * Creates an array of the own enumerable symbols of `object`.
       *
       * @private
       * @param {Object} object The object to query.
       * @returns {Array} Returns the array of symbols.
       */
      function getSymbols(object) {
        if (object == null) {
          return[]}// Object(Object) Compatible with ES5
        object = Object(object)
        // Returns the enumerable symbol property
        return nativeGetSymbols(object).filter((symbol) = > propertyIsEnumerable.call(object, symbol))
      }
      Copy the code
    • Untiled copy. In contrast to tiled copy, non-tiled copy is much easier because you don’t need to get the attributes on the prototype chain. You can just use Object.assign to generate a new Object. Copy in the corresponding Symbol property. The source code is as follows:

    copySymbols(value, Object.assign(result, value))
    Copy the code

    In contrast to the copySymbolsIn function, the copySymbols function internally calls the getSymbols function instead of the getSymbolsIn function. The source code is as follows:

    function copySymbols(source, object) {
      return copyObject(source, getSymbols(source), object)
    }
    Copy the code
  • Deep copy

    The following code partially modifies the source code

    • Tile copy. It’s just a different way of getting the key than a non-tiled copy. callgetAllKeysInFunction, source code as follows:
    const props = getAllKeysIn(value)
    Copy the code
    • Untiled copy. callgetAllKeysFunction, source code as follows:
    const props = getAllKeys(value)
    Copy the code

    We then call the forEach (arrayEach) method on the props array and recursively call the baseClone and assignValue functions internally. The source code is as follows:

    arrayEach(props, (subValue, key) = > {
      // props e.g ['name', 'age']
      // key = 'name'
      // subvalue = value['name']
      key = subValue
      subValue = value[key]
      // Recursively populate clone (susceptible to call stack limits).
      // result[key] = baseClone(subValue, bitmask, customizer, key, value, stack)
      assignValue(result, key, baseClone(subValue, bitmask, customizer, key, value, stack))
    })
    
    Copy the code

Symbol

The typeof the object is obtained via the getTag function (internal call to typeof returns [object Symbol]). This is then passed to the initCloneByTag function to generate a Symbol instance. InitCloneByTag function:

const symbolTag = '[object Symbol]'
function initCloneByTag(object, tag, isDeep) {
  // Get a reference to the object constructor
  const Ctor = object.constructor
  switch (tag) {
    ...
    case symbolTag:
      return cloneSymbol(object)
  }
}
Copy the code

The cloneSymbol function internally calls the symbol.prototype.valueof method, which is passed to Object() as an argument to generate a new Object. The source code is as follows:

const symbolValueOf = Symbol.prototype.valueOf
function cloneSymbol(symbol) {
  return Object(symbolValueOf.call(symbol))
}

Copy the code

ValueOf the user gets the original valueOf an object, so you don’t need to call this method manually. JavaScript automatically calls it when it encounters an object that needs the original value

Why not pass symbol directly to Object? Because the symbol.prototype. valueOf method gets Primitive values, Primitive is not an object and does not contain any methods, which means that the generated new object does not affect the old object.

ArrayBuffer

The ArrayBuffer object is used to represent a generic, fixed-length buffer of raw binary data. You can’t manipulate the contents of an ArrayBuffer directly, but rather through a type array object or DataView object, which represents the data in the buffer in specific formats and reads and writes the contents of the buffer in those formats.

Each byte can store eight binary digits 0x00-0xFF

Similar to Symbol, the corresponding type is obtained through the getTag function and passed to initCloneByTag, which calls the corresponding cloneArrayBuffer function. The source code is as follows:

const arrayBufferTag = '[object ArrayBuffer]'

function initCloneByTag(object, tag, isDeep) {
  // Get a reference to the object constructor
  const Ctor = object.constructor
  switch (tag) {
    ...
    case arrayBufferTag:
      returncloneArrayBuffer(object) ... }}Copy the code

Since we can’t manipulate ArrayBuffer directly, we need a medium, which is the Uint8Array, the TypedArray array. Since computers are based on bytes, the 8-bit Uint8Array is just enough to handle the data we need. The source code for cloneArrayBuffer is as follows:

function cloneArrayBuffer(arrayBuffer) {
  // Based on the memory address length of the original arrayBuffer
  // Generate a new memory address
  const result = new arrayBuffer.constructor(arrayBuffer.byteLength)
  // Since we can't manipulate the arrayBuffer directly, we need a TypeDarray array to help us copy the memory
  // Therefore use a Uint8Array typedArray array, new Uint8Array(result)
  // Call the set method, which receives an array or typedArray, so we need to initialize the original ArrayBuffer as a Uint8Array array as well
  // Why use Uint8Array?
  // The basic unit of a computer is the byte, and each byte is 8 bits of base 2, so the Uint8Array is sufficient
  new Uint8Array(result).set(new Uint8Array(arrayBuffer))
  return result
}
Copy the code

DataView

DataView is a low-level interface that can read and write multiple numeric types from binary ArrayBuffer objects, regardless of platform endianness.

It is initialized by the initCloneByTag function again. The source code is as follows:

const dataViewTag = '[object DataView]'

function initCloneByTag(object, tag, isDeep) {
  // Get a reference to the object constructor
  const Ctor = object.constructor
  switch (tag) {
    ...
    case dataViewTag:
      returncloneDataView(object, isDeep) ... }}Copy the code

CloneDataView passes in the depth copy identifier isDeep, which means that the depth copy is different.

  • Deep copy because DataView is used to manipulate a binary buffer. So in the case of deep copy, the original memory is stored in a new location. The original arrayBuffer is used to generate a new arrayBuffer. The cloneArrayBuffer above already does this. The source code is as follows:

    const buffer = cloneArrayBuffer(dataView.buffer)
    Copy the code
  • Shallow copy For shallow copies, you can reference the original memory address. The source code is as follows:

    const buffer =  dataView.buffer
    Copy the code

    To sum up, the final code of cloneDataView:

function cloneDataView(dataView, isDeep) {
  const buffer = isDeep ? cloneArrayBuffer(dataView.buffer) : dataView.buffer
  return new dataView.constructor(buffer, dataView.byteOffset, dataView.byteLength)
}
Copy the code

Dataview. byteOffset and dataView.byteLength are used to ensure the same operating range with the original dataView object.

TypedArray

A TypedArray object represents an array-like view of an underlying binary data buffer. In fact, there is no global property named TypedArray, and there is no constructor named TypedArray. Instead, there are many different global properties whose values are typed array constructors of specific element types,

These class arrays are:

const float32Tag = '[object Float32Array]'
const float64Tag = '[object Float64Array]'
const int8Tag = '[object Int8Array]'
const int16Tag = '[object Int16Array]'
const int32Tag = '[object Int32Array]'
const uint8Tag = '[object Uint8Array]'
const uint8ClampedTag = '[object Uint8ClampedArray]'
const uint16Tag = '[object Uint16Array]'
const uint32Tag = '[object Uint32Array]'
Copy the code

Lodash initialization method, initCloneByTag function initialization. The source code is as follows:

function initCloneByTag(object, tag, isDeep) {
  // Get a reference to the object constructor
  const Ctor = object.constructor
  switch (tag) {
    ...
    case float32Tag: case float64Tag:
    case int8Tag: case int16Tag: case int32Tag:
    case uint8Tag: case uint8ClampedTag: case uint16Tag: case uint32Tag:
      returncloneTypedArray(object, isDeep) ... }}Copy the code

CloneTypedArray passes in the depth copy identifier isDeep, which means that the depth copy is different. Since typedArray and DataView are used to manipulate memory, the light and depth copies work the same way. The source code is as follows:

function cloneTypedArray(typedArray, isDeep) {
  const buffer = isDeep ? cloneArrayBuffer(typedArray.buffer) : typedArray.buffer
  return new typedArray.constructor(buffer, typedArray.byteOffset, typedArray.length)
}
Copy the code

In baseClone, for objects of type TypedArray, return:

if (isTypedArray(value)) {
    return result
  }
Copy the code

The isTypedArray function that determines whether the value passed is an instance of TypedArray. For browser environments, you can use regular expressions to match toStringTag. For Node environments, you can call the types. IsTypedArray method in the Util module. The source code is as follows:

// Check the browser environment
const reTypedTag = /^\[object (?:Float(?:32|64)|(?:Int|Uint)(?:8|16|32)|Uint8Clamped)Array\]$/
// https://nodejs.org/api/util.html#util_util_types
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray
// node
const nodeIsTypedArray = nodeTypes && nodeTypes.isTypedArray

const isTypedArray = nodeIsTypedArray
  // The node environment calls the isTypedArray method on util.types
  ? (value) = > nodeIsTypedArray(value)
  // object | function
  // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray#typedarray_objects
  : (value) = > isObjectLike(value) && reTypedTag.test(getTag(value))

Copy the code

So how does Lodash tellnodeEnvironment and returnnodeTypes(require('util').types)?

Check whether the Node environment globalThis refers to global.Object === Object. Source code is as follows:

const freeGlobal = typeof global= = ='object' && global! = =null && global.Object === Object && global

Copy the code

Module, exports, and module.exports detect context. The source code is as follows:

/** Detect free variable `exports`. */
const freeExports = typeof exports= = ='object' && exports! = =null&&!exports.nodeType && exports

/** Detect free variable `module`. */
const freeModule = freeExports && typeof module= = ='object' && module! = =null&&!module.nodeType && module

/** Detect the popular CommonJS extension `module.exports`. */
const moduleExports = freeModule && freeModule.exports === freeExports
Copy the code

The handling of modules is mainly to distinguish between ESMs, as shown here

Finally return the utility function we need:

const nodeTypes = ((() = > {
  try {
    /* Detect public `util.types` helpers for Node.js v10+. */
    /* Node.js deprecation code: DEP0103. */
    const typesHelper = freeModule && freeModule.require && freeModule.require('util').types
    return typesHelper
      ? typesHelper
      /* Legacy process.binding('util') for Node.js earlier than v10. */
      : freeProcess && freeProcess.binding && freeProcess.binding('util')}catch (e) {}
})())
Copy the code

See here for node.js Deprecation code: DEP0103

Buffer

Buffer is a Node type, and Lodash’s handling is simple:

if (isBuffer(value)) {
  return cloneBuffer(value, isDeep)
}
Copy the code

The cloneBuffer function has a bug where the depth copy is reversed

const Buffer = moduleExports ? root.Buffer : undefined, allocUnsafe = Buffer ? Buffer.allocUnsafe : undefined

function cloneBuffer(buffer, isDeep) {
  // Deep copy
  // It used to be isDeep
  if(! isDeep) {// Returns a new Buffer that references the same memory as the original
    return buffer.slice()
  }
  const length = buffer.length
  // Generate a new block of memory (possibly containing dirty data)
  const result = allocUnsafe ? allocUnsafe(length) : new buffer.constructor(length)

  // Copy the original data into the new memory
  buffer.copy(result)
  return result
}
Copy the code

For deep copy, you essentially use buffer. allocUnsafe to generate a new block of memory and copy the data from the old memory.

The root function returns this object from the current context.

const freeGlobal = typeof global= = ='object' && global! = =null && global.Object === Object && global

/** Detect free variable `globalThis` */
const freeGlobalThis = typeof globalThis === 'object'&& globalThis ! = =null && globalThis.Object == Object && globalThis

/** Detect free variable `self`. */
// https://developer.mozilla.org/en-US/docs/Web/API/Window/self
const freeSelf = typeof self === 'object'&& self ! = =null && self.Object === Object && self

/** Used as a reference to the global object. */
const root = freeGlobalThis || freeGlobal || freeSelf || Function('return this') ()Copy the code

Function(‘return this’)()

Map Set

For both types, a corresponding Map or Set instance is initialized based on toStringTag. The source code is as follows:

const setTag = '[object Set]'
const mapTag = '[object Map]'

function initCloneByTag(object, tag, isDeep) {
  // Get a reference to the object constructor
  const Ctor = object.constructor
  switch (tag) {
    ...
    case mapTag:
      return new Ctor
    ...
    case setTag:
      return newCtor ... }}Copy the code

Note that since no initialization parameters are required, the instance is created using new Ctor directly, the same way as calling new Ctor() directly.

Lodash uses deep copy for Map and Set values by default:

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

  / / set type
  if (tag == setTag) {
    value.forEach((subValue) = > {
      result.add(baseClone(subValue, bitmask, customizer, subValue, value, stack))
    })
    return result
  }
Copy the code

More references to maps and sets

Date

Similar to the underlying data type, the constructor is called directly and the date.prototype. valueOf method (+object) is implicitly called. The code for initCloneByTag is as follows:

const dateTag = '[object Date]'
function initCloneByTag(object, tag, isDeep) {
  // Get a reference to the object constructor
  const Ctor = object.constructor
  switch (tag) {
    ...
    case dateTag:
      return newCtor(+object) ... }}Copy the code

RegExp

The code for initCloneByTag is as follows:

const regexpTag = '[object RegExp]'
function initCloneByTag(object, tag, isDeep) {
  // Get a reference to the object constructor
  const Ctor = object.constructor
  switch (tag) {
    ...
    case regexpTag:
      returncloneRegExp(object) ... }}Copy the code

For the cloneRegExp method. One difference with structured cloning is that it copies the lastIndex of the original regular expression instance. At the same time, you need to get the flags of the original regular expression. The source code is as follows:

const reFlags = /\w*$/
function cloneRegExp(regexp) {
  // reflags. exec(regexp) returns the matching flags array, guessing that regEXP implicitly calls toString to combine flags and the regular string
  const result = new regexp.constructor(regexp.source, reFlags.exec(regexp))
  // Since lastIndex was reset to 0, the lastIndex is reassigned here
  result.lastIndex = regexp.lastIndex
  return result
}
Copy the code

Copied objects are not supported by default

  • function
  • [object WeakMap]
  • [object Error]This object is supported in structured cloning algorithms

baseCloneThe source code for reference

Distinguish deep and shallow copies by bit operation

The baseClone function uses a bitmask parameter to perform a bit operation to determine which copy functions are enabled, as follows:

/ / 0001
const CLONE_DEEP_FLAG = 1
/ / 0010
const CLONE_FLAT_FLAG = 2
/ / 0100
const CLONE_SYMBOLS_FLAG = 4

// Deep clone 0001
const isDeep = bitmask & CLONE_DEEP_FLAG
// Tile clone 0010
const isFlat = bitmask & CLONE_FLAT_FLAG
// contains Symbols 0100
const isFull = bitmask & CLONE_SYMBOLS_FLAG
Copy the code

The advantage of this is that by passing the corresponding type, the corresponding copy mode can be turned on, similar to the switch function:

CLONE_DEEP_FLAG | CLONE_FLAT_FLAG | CLONE_SYMBOLS_FLAGCopy the code

Through a | operation, the above parameters are passed to bitmask, can open all of the three functions. At the same time, the performance of bit operation is the highest, and the problem of writing conditional statements or switch statements in a function is avoided. There are a lot of similar scenarios in the business, so we can refer to the implementation of Bitmask to optimize our code.

Object repeated reference processing

Stack function. Depends on the ListCache and MapCache classes and is initialized to ListCache by default. When the ListCache size reaches the 200 limit, it is automatically converted to MapCache. MapCache initializes three Map objects, including HashMap (Object storage data created by Object.create(NULL)) and native Map

ListCache and MapCache are implemented in simple ways, similar to the following pattern:

class cache {
  constructor(entries) {
    // Store data structures
    this.__data__ = ...
    // The number of data currently stored
    this.size
    // Initialize according to entries. }// Get the corresponding key value
  get(key){... }// Set the value of the corresponding key
  set(key, value){... }// Whether to contain the corresponding key
  has(key){... }/ / to empty
  clear() {
    this.__data__ = ...
    this.size = 0. }// Delete an item
  delete(key){... }}Copy the code

MapCache = MapCache; ListCache = MapCache; MapCache = MapCache; ListCache = MapCache; The source code is as follows:

set(key, value) {
    // This is stack's this.__data__, which refers to ListCache or MapCache instances
    let data = this.__data__
    if (data instanceof ListCache) {
      // Paris here gets the ListCache instance's __data__, where the data is stored
      const pairs = data.__data__
      // If the maximum array length is exceeded, use MapCache instead of listCache
      if (pairs.length < LARGE_ARRAY_SIZE - 1) {
        pairs.push([key, value])
        this.size = ++data.size
        return this
      }
      // Replace the current ListCache with MapCache and pass MapCache initialization with saving pairs
      data = this.__data__ = new MapCache(pairs)
    }
    data.set(key, value)
    this.size = data.size
    return this
  }
Copy the code

ListCache stores data in the following ways compared with MapCache (HashMap) :

  • ListCache
this.__data__ = [[key, value], ...]
Copy the code
  • HashMap
this.__data__ = {key: value}
Copy the code

This is not necessarily correct, I refer to the List and Map in Java to explain

Compared with a Map, a List stores data in an ordered order. Therefore, it can quickly locate elements when deleting them. Map can be deleted only after the elements are obtained through GET.

Take a look at how ListCache removes the last element of the array, which is also a performance optimization point:

delete(key) {
    const data = this.__data__
    // Get the index of the current key
    const index = assocIndexOf(data, key)

    if (index < 0) {
      return false
    }
    const lastIndex = data.length - 1
    // The last item is direct pop
    if (index == lastIndex) {
      data.pop()
    } else {
    // Otherwise, delete the corresponding item
      data.splice(index, 1)} -this.size
    return true
  }
Copy the code

Compared to Map, HashMap replaces the GET method by directly passing the property key and therefore performs better than Map. So how does MapCache determine when to store data using a HashMap and when to store data using a native Map? MapCache initializes this.__data__ to the following form:

this.__data__ = {
                  'hash': new Hash,
                  'map': new Map.'string': new Hash
                }
Copy the code

When the instance calls the set method, the getMapData function is called to select which map to store in based on the type of key

function getMapData({ __data__ }, key) {
  const data = __data__
  // Check whether key is __proto__ or null
  return isKeyable(key)
  // data[string] data[hash]
    ? data[typeof key === 'string' ? 'string' : 'hash']
    // If the key is not a regular attribute, use the native map
    : data.map
}
Copy the code

IsKeyable:

function isKeyable(value) {
  const type = typeof value
  return (type === 'string' || type === 'number' || type === 'symbol' || type === 'boolean')? (value ! = ='__proto__')
    // hashMap allows keys to be null
    : (value === null)}Copy the code

Finally, let’s look at MapCache’s set function:

set(key, value) {
    const data = getMapData(this, key)
    const size = data.size

    data.set(key, value)
    this.size += data.size == size ? 0 : 1
    return this
  }
Copy the code

Reference documentation

Lodash baseClone source

MDN

That’s all for this article. If it was helpful, please give it a thumbs up and spread it to more people