preface

This paper introduces in detail the assignment, shallow copy, deep copy, which involves some knowledge points are also introduced: JS data type and judgment, object traversal, WeakMap, SEVERAL ways of JS array traversal performance comparison and so on.

The assignment

  1. a = b

  2. Basic data type: Two variables do not affect

  3. Reference data types: copy address only, two variables refer to the same address, a and B to the same object

Js data type and judgment

Shallow copy

  1. Unlike assignment, copy a layer of objects

  2. If the property is of a primitive type, the value of the primitive type is copied; if the property is of a reference type, the memory address is copied.

  3. Normal array and object assignments and methods are shallow copies, for example:

    • Object.assign(a, b) : Copies all enumerable attributes from one or more objects to the target Object, returning the target Object.
    • Array concat (), array. Prototype. Slice ()
    • […a]
  4. Code implementation:

    
    const shallowClone = (target) = > {
      if (typeof target === 'object'&& target ! = =null) { // Object, array, but not null
        const cloneTarget = Array.isArray(target) ? [] : {}
    
        for (item in target) { 
          if (target.hasOwnProperty(item)) { // Whether it is its own attribute (non-inherited)
            cloneTarget[item] = target[item]
          }
        }
        return cloneTarget
      } else {
        return target // Base type returns directly}}Copy the code

Determines whether an object attribute exists

  1. HasOwnProperty () can only detect its own properties.

  2. In can detect both owned and inherited properties.

  3. Use! == detect, use! The value of the object attribute cannot be set to undefined. The value must be! ==, not! = because! = undefined and null are not distinguished

And assignment


let a = {
  name: 1.address: {
    city: 2.home: 3}}let b = a / / assignment
let c = shallowClone(a) / / shallow copy

a.name = 4
a.address.city = 5

console.log(a) // {name: 4, address: {city: 5, home: 3}}
console.log(b) // {name: 4, address: {city: 5, home: 3}
console.log(c) // {name: 1, address: {city: 5, home: 3}} // Shallow copy

Copy the code

Deep copy

  1. Deep copy is to make a full copy of an object out of memory, creating a new area of heap memory to store the new object.

Copy exactly one object, including the address, without affecting each other. Nested objects are also fully copied.

  1. The beggar version:

    
    JSON.parse(JSON.stringify(obj))
    
    Copy the code
    
    jQuery.extend()
    
    Copy the code
  2. Existing problems:

    • Unable to resolve circular reference, an error is reported
    • Special objects such as functions, RegExp, Date, Set, and Map cannot be copied.
    • Undefined and symbol are ignored.
    • Non-serializable function
  3. Library of tools: LoDash’s cloneDeep method

implementation

Reference juejin. Cn/post / 690206… And segmentfault.com/a/119000002…

Basic Version:

Consider only ordinary objects and arrays


const cloneDeep = (target) = > {
  
  if (typeof target === 'object'&& target ! = =null) { // Object, array, but not null
    const cloneTarget = Array.isArray(target) ? [] : {}

    for (item in target) { 
      if (target.hasOwnProperty(item)) { // Whether it is its own attribute (non-inherited)
        cloneTarget[item] = cloneDeep(target[item])
      }
    }
    return cloneTarget
  } else {
    return target // Base type returns directly}}Copy the code

Circular reference:

  1. Attributes of an object reference itself either indirectly or directly, for example:

    
    const target = {
      a: 1.b: {
          bb: 2
      },
    }
    target.c = target
    
    Copy the code
  2. Solve the problem of circular reference: we can create a extra storage space, to store the current objects and copy the object correspondence, when need to copy the current object, go to the storage space, find ever copy this object, if any direct return, if not to copy, so clever resolve problem of circular references.

This storage space needs to be able to store key-value data, and the key can be a reference type, which naturally leads to the data structure Map.

  1. Ideas:

    • Check whether objects have been cloned in the map
    • Yes. – Straight back
    • None – Stores the current object as key and the cloned object as value
    • Continue to clone
  2. So the code could be written like this:

    
    const cloneDeep = (target, map = new Map(a)) = > {
      
      if (typeof target === 'object'&& target ! = =null) { // Object, array, but not null
        const cloneTarget = Array.isArray(target) ? [] : {}
    
        // Resolve circular references
        if (map.get(target)) return map.get(target)
        map.set(target, cloneTarget)
    
        for (item in target) { 
          if (target.hasOwnProperty(item)) { // Whether it is its own attribute (non-inherited)
            cloneTarget[item] = cloneDeep(target[item])
          }
        }
        return cloneTarget
      } else {
        return target // Base type returns directly}}Copy the code
  3. Optimization: Use WeakMap to replace Map

WeakMap

What is WeakMap?

A WeakMap object is a set of key/value pairs where the keys are weakly referenced. The key must be an object, and the value can be arbitrary.

What is a weak reference?

In computer programming, a weak reference, as opposed to a strong reference, is a reference that cannot be guaranteed that the object it refers to will not be collected by the garbage collector. An object that is referred to only by weak references is considered inaccessible (or weakly accessible) and can therefore be reclaimed at any time.

Const obj = {} by default, we create a strongly referenced object. If we manually set obj = null, it will be collected by the garbage collector. If it is a weakly referenced object, it will be collected automatically.

Here’s an example:


let obj1 = { a: 1 }
let obj2 = new Map()

obj2.set(obj, 2)

obj1 = null // Free obj1

Copy the code

Although we have freed obj1 above, there is a strong reference inertial frame between obj1 and obj2, so this part of memory still cannot be freed.

Change the above map to WeakMap, target and obj have weak reference relationship, when the next garbage collection mechanism is executed, this block of memory will be freed.

So:

Imagine that if the object we want to copy is very large, using Map will cause a very large extra consumption of memory, and we need to manually clear the Map attributes to release the memory, while WeakMap will help us solve this problem subtly.

Other data types

  1. I’ve only considered ordinary objects and arrays above, but there are many more data types. So we need to discuss in different cases:

    • Basic data types are returned directly.
    • The set, the mapAnd so onSustainable ergodic, functions, re, etc. are not sustainable traversal and need to be cloned separately.
  2. Const cloneTarget = array.isarray (target)? [] : {} gets their initialization data. This can be obtained generically by getting constructor.

    
    // Get initialization data
    const cloneTarget = new target.constructor()
    
    Copy the code

    Const target = {} is the syntactic sugar for const target = new Object(). This approach has another advantage: because we are also using the constructor of the original object, it retains the data on the object’s prototype, which would have been lost if we had just used the plain {}.

  3. Get the exact reference type: the toString() method.

Each reference type has a toString() method, which is inherited by every Object by default. If this method is not overridden in a custom object, toString() returns “[Object type]”, where type is the type of the object.

Note that toString() only works if the method is not overridden in a custom object. In fact, most reference types such as Array, Date, RegExp, etc., override toString().

So: call the toString() method not overridden on the Object prototype and use call to change the this pointer to get the exact reference type.

Object.prototype.toString.call(target)

1. Determine the reference type

Check whether it is a reference type accurately by typeof:


// Basic type returns directly
if (target === null| | -typeoftarget ! = ='object' && typeoftarget ! = ='function')) {
  return target
}

Copy the code
2. Type of traversal that can continue
  1. The objects and arrays we have considered above are the types that can continue traversal, because they can also store other types of data in memory, as well as Map, Set, etc., which can continue traversal. We will only consider these four types here, so you can continue to explore other types if you are interested.

  2. Map, Set cannot add attributes like array objects, nor can it be iterated with for in, so clone Map, Set:


// Get the data type method
const getType = (target) = > {
  return Object.prototype.toString().call(target)
}

// set
if (getType(target) === '[object Set]') {
  target.forEach(item= > cloneTarget.add(cloneDeep(item, map)))
}

// map and key can be objects
if (getType(target) === '[object Map]') {
  target.forEach((item, key) = > cloneTarget.set(cloneDeep(key, map), cloneDeep(item, map)))
}

Copy the code
3. Type that cannot continue traversal

Bool, Number, String, Date, Error, Symbol, re, and function traversals cannot continue.

1. Bool, Number, String, Date, ErrorWrapper object
  1. This refers to the following:

    
    console.log(typeof new Number(1)) // object
    
    Copy the code
  2. For each of these types, we can create a new object directly from the constructor and raw data.

  3. Implementation:

    
    Bool, Number, String, Date, Error wrapper objects
    let otherObj = [
      '[object Boolean]'.'[object Number]'.'[object String]'.'[object Date]'.'[object Error]'
    ]
    
    if (otherObj.includes(getType(target))) {
      return new cloneTarget(target)
    }
    
    Copy the code
Clone the Symbol wrapper object
  1. For Symbol, see Symbol

  2. Symbol.prototype.valueof (), which returns the original valueOf the Symbol contained in the current Symbol object.

  3. Let’s talk about valueOf and toString

  4. The Object constructor wraps the given value as a new Object.

    • If the given value is null or undefined, it creates and returns an empty object.
    • Otherwise, it returns an object of the type corresponding to the given value.
    • If the given value is an existing object, the existing value (same address) is returned.

    Object and new Object() behave the same when called in a non-constructor context.

  5. Implementation:

    
    // Symbol wraps the object
    if (getType(target) === '[object Symbol]') {
      return cloneSymbol(target)
    }
    
    Clone the Symbol wrapper object method
    function cloneFunction(target) {
      return Object(Symbol.prototype.valueOf.call(target))
    }
    
    Copy the code
3. Clone the re
  1. How to clone a regular?

  2. Implementation:

    
    / / regular
    if (getType(target) === '[object Symbol]') {
      return cloneSymbol(target)
    }
    
    // Clone the regular method
    function cloneReg (target) {
      const reFlags = /\w*$/
      const result = new target.constructor(target.source, reFlags.exec(target))
      result.lastIndex = target.lastIndex
      return result
    }
    
    Copy the code
4. Clone functions
  1. Distinguish arrow functions from normal functions: by prototype, arrow functions have no prototype.

  2. Clone arrow function: Use eval and the function string directly to regenerate an arrow function. Note that this method does not work with normal functions.

  3. Clone normal functions: Extract the Function body and Function parameters using the re respectively, and then reconstruct a new Function using the new Function ([arg1[, arg2[,…argN]],] functionBody) constructor.

  4. Implementation:

    
    // Clone function
    if (getType(target) === '[object Function]') {
      return cloneFunction(target)
    }
    
    // Clone function method
    function cloneFunction (func) {
      const bodyReg = / (? <={)(.|\n)+(? =})/m
      const paramReg = / (? < = \ () + (? =\)\s+{)/
      const funcString = func.toString()
    
      if (func.prototype) {
          console.log('Ordinary function')
          const param = paramReg.exec(funcString)
          const body = bodyReg.exec(funcString)
          if (body) {
            console.log('Match to function body:', body[0])
            if (param) {
              const paramArr = param[0].split(', ')
              console.log('Match to argument:', paramArr)
              return new Function(... paramArr, body[0])}else {
              return new Function(body[0])}}else {
            return null}}else {
        return eval(funcString)
      }
    }
    
    Copy the code

The final code


// Get the data type method
function getType (target) = >{
  return Object.prototype.toString().call(target)
}

Clone the Symbol wrapper object method
function cloneFunction(target) {
  return Object(Symbol.prototype.valueOf.call(target))
}

// Clone the regular method
function cloneReg (target) {
  const reFlags = /\w*$/
  const result = new target.constructor(target.source, reFlags.exec(target))
  result.lastIndex = target.lastIndex
  return result
}

// Clone function method
function cloneFunction (func) {
  const bodyReg = / (? <={)(.|\n)+(? =})/m
  const paramReg = / (? < = \ () + (? =\)\s+{)/
  const funcString = func.toString()

  if (func.prototype) {
      console.log('Ordinary function')
      const param = paramReg.exec(funcString)
      const body = bodyReg.exec(funcString)
      if (body) {
        console.log('Match to function body:', body[0])
        if (param) {
          const paramArr = param[0].split(', ')
          console.log('Match to argument:', paramArr)
          return new Function(... paramArr, body[0])}else {
          return new Function(body[0])}}else {
        return null}}else {
    return eval(funcString)
  }
}

const cloneDeep = (target, map = new WeakMap(a)) = > {
  // Map is strongly referenced, and attributes need to be cleared manually to free memory.
  // WeakMap weak reference, may be garbage collection at any time, so that memory timely release, is the only choice to solve circular reference.

  // Basic type returns directly
  if (target === null| | -typeoftarget ! = ='object' && typeoftarget ! = ='function')) {
    return target
  }

  // Resolve circular references
  if (map.get(target)) return map.get(target)
  map.set(target, cloneTarget)

  // Get initialization data
  const cloneTarget = new target.constructor()

  Bool, Number, String, Date, Error wrapper objects
  let otherObj = [
    '[object Boolean]'.'[object Number]'.'[object String]'.'[object Date]'.'[object Error]'
  ]
  if (otherObj.includes(getType(target))) {
    return new cloneTarget(target)
  }

  // Symbol wraps the object
  if (getType(target) === '[object Symbol]') {
    return cloneSymbol(target)
  }

  / / regular
  if (getType(target) === '[object Symbol]') {
    return cloneSymbol(target)
  }

  // Clone function
  if (getType(target) === '[object Function]') {
    return cloneFunction(target)
  }

  // set
  if (getType(target) === '[object Set]') {
    target.forEach(item= > cloneTarget.add(cloneDeep(item, map)))
  }

  // map and key can be objects
  if (getType(target) === '[object Map]') {
    target.forEach((item, key) = > cloneTarget.set(cloneDeep(key, map), cloneDeep(item, map)))
  }

  // Common objects and arrays
  // Set and Map cannot be traversed using for in
  for (let key in target) {
    if (target.hasOwnProperty(key)) {
      cloneObj[key] = cloneDeep(target[key], map)
    }
  }
}

Copy the code

Todo performance optimization

We can replace for in traversal with while.

JS array traversal several ways of performance comparison
  1. Normal for loop

    
    for(j = 0; j < arr.length; j++) {} 
    
    Copy the code

    The simplest one, and one of the most frequently used, is not weak, but there is still room for optimization.

  2. Optimized version for loop

Use temporary variables to cache the length and avoid fetching the array length repeatedly. The optimization effect is more obvious when the array is large.

```js for(j = 0, n = arr.length; j < n; J++) {} ' 'the highest performance of all loop traversal methods.Copy the code
  1. The foreach loop

    
    arr.forEach(() = > {})
    
    Copy the code

    Performance is weaker than normal for loops.

  2. The foreach variant

    
    Array.prototype.forEach.call(args, () = > {})
    
    Copy the code

    Performance is weaker than regular foreach

  3. The for loop in

    
    for(j in arr) {}
    
    Copy the code

    It’s the least efficient

  4. The map traverse

    
    arr.map(() = > {})
    
    Copy the code

    Not as efficient as Foreach

  5. The for of traverse

    
    for(let j of arr) {}
    
    Copy the code

    Better performance than for in, but still not as good as normal for loops

  6. While loops perform better than normal for loops

reference

  1. “Intermediate advanced front-end interview” handwritten code collection
  2. How to write a deep copy that will impress your interviewer?
  3. JS several array traversal methods and performance analysis comparison