This is the first day of my participation in Gwen Challenge

I wrote a previous article: Deep and light copy of JavaScript, it’s not that hard! But the copy processing inside is obviously not ideal.

Let’s talk more about it today…

Json.stringify () defects

Using JavaScript’s built-in JSON processing functions, you can implement easy deep copy:

const obj = {
  // ...
}
JSON.parse(JSON.stringify(obj)) // Serialize and deserialize
Copy the code

This approach, in fact, can be applied to more than 90% of the application scenarios. After all, in most projects, you rarely copy a function or anything.

But it has to be said that there are potholes, which are created by the implementation logic of the json.stringify () method itself:

JSON.stringify(value[, replacer[, space]])

The method has the following characteristics:

  • Boilervalues, numeric values, and string wrapper objects are automatically converted to their original values during serialization.
  • undefined,Arbitrary function,Symbol valueThere are two different situations in the serialization process. If it appears in an attribute value of a non-array object, it is ignored. If it appears in an array, it is converted tonull.
  • Arbitrary function,undefinedReturns when converted separatelyundefined.
  • allAttribute with Symbol as the attribute keyIs completely ignored, even in the second argument of this methodreplacerThe property is specified in.
  • The Date DateCalls its built-intoJSON()Method is converted to a string and is therefore processed by the original string.
  • NaNInfinityThe numerical andnullWill be asnull.
  • These objectsMap,Set,WeakMap,WeakSetOnly enumerable attributes are serialized.
  • Converted value if containstoJSON()Method that defines what values will be serialized.
  • To containA circular referenceObject, throws an error.

Deep copy boundaries

In fact, there are so many cases that can’t be handled against the two built-in global methods, isn’t it annoying? I suspect that json.parse () and json.stringify () just make it easier to manipulate JSON-formatted JavaScript objects or jSON-formatted strings.

As for the “pit” mentioned above, it clearly does not meet the format requirements for cross-platform data exchange. In JSON, it has null, which means there is no undefined, Symbol type, function, etc.

JSON is a data format, or a specification. JSON is used for cross-platform data exchange, independent of language and platform. A JavaScript object, on the other hand, is an instance that exists in memory. JavaScript objects cannot be transferred until they are serialized as JSON strings.

I wrote a previous article that covered the relationship between JSON and JavaScript and some of the details of the two methods mentioned above. See: JSON and JavaScript in detail.

If their implementation of a deep copy of the method, in fact, there are a lot of boundary problems to deal with, as for these various boundary cases, whether to deal with the best from the actual situation.

What are the common boundary cases?

There are mainly circular reference, wrapped object, function, prototype chain, non-enumerable attribute, Map/WeakMap, Set/WeakSet, RegExp, Symbol, Date, ArrayBuffer, native DOM/BOM object, etc.

By far the most complete third party deep-copy method is the _.clonedeep () method of the Lodash library. In a real-world project, I would recommend it for cases where json.stringify () cannot resolve them. Otherwise, use the built-in JSON method; there is no need for complications.

But in order to learn deep copy, you should try to implement it in every case, which I think is what you’re reading this article about. This way, whether it’s implementing a deep copy of a particular request, or interviewing for a job, you can handle it in stride.

Here to learn together, if there is insufficient, welcome to point out 👋 ~

Third, self-implementation of deep copy method

The main use of recursive ideas to achieve a deep copy method.

PS: The full deep-copy method will be available at the end of this article.

Start with a simple version:

const deepCopy = source= > {
  // Check whether it is an array
  const isArray = arr= > Object.prototype.toString.call(arr) === '[object Array]'

  // Check whether it is a reference type
  const isObject = obj= >obj ! = =null && (typeof obj === 'object' || typeof obj === 'function')

  // Copy (recursive)
  const copy = input= > {
    if (typeof input === 'function'| |! isObject(input))return input

    const output = isArray(input) ? [] : {}
    for (let key in input) {
      if (input.hasOwnProperty(key)) {
        const value = input[key]
        output[key] = copy(value)
      }
    }

    return output
  }

  return copy(source)
}
Copy the code

There are a number of special cases to deal with in the simplified version above, so we’ll work on improving json.stringify () bit by bit.

3.1 Processing of bools, values and strings

Note that creating an explicit wrapper object around a raw data type is no longer supported starting with ES6. However, existing original wrapper objects (such as New Boolean, New Number, New String) are still available for legacy reasons. This is why ES6+ ‘s new Symbol and BigInt data types cannot create instance objects using the new keyword.

Because of the for… In cannot iterate over non-enumerable properties. For example, the [[PrimitiveValue]] inner property of the wrapped object requires special treatment.

The above results, obviously not the expected results. The [[PrimitiveValue]] property of the wrapper object can be obtained using valueOf().

const deepCopy = source= > {
  // Get data type (this time added)
  const getClass = x= > Object.prototype.toString.call(x)

  // Check whether it is an array
  const isArray = arr= > getClass(arr) === '[object Array]'

  // Check whether it is a reference type
  const isObject = obj= >obj ! = =null && (typeof obj === 'object' || typeof obj === 'function')

  // Check whether it is a package object (added this time)
  const isWrapperObject = obj= > {
    const theClass = getClass(obj)
    const type = /^\[object (.*)\]$/.exec(theClass)[1]
    return ['Boolean'.'Number'.'String'.'Symbol'.'BigInt'].includes(type)
  }

  // Process the packaging object (this time added)
  const handleWrapperObject = obj= > {
    const type = getClass(obj)
    switch (type) {
      case '[object Boolean]':
        return Object(Boolean.prototype.valueOf.call(obj))
      case '[object Number]':
        return Object(Number.prototype.valueOf.call(obj))
      case '[object String]':
        return Object(String.prototype.valueOf.call(obj))
      case '[object Symbol]':
        return Object(Symbol.prototype.valueOf.call(obj))
      case '[object BigInt]':
        return Object(BigInt.prototype.valueOf.call(obj))
      default:
        return undefined}}// Copy (recursive)
  const copy = input= > {
    if (typeof input === 'function'| |! isObject(input))return input

    // Process the packaging object (this time added)
    if (isWrapperObject(input)) {
      return handleWrapperObject(input)
    }

    // The rest remains the same. To reduce the length, 10,000 words were omitted...
  }

  return copy(source)
}
Copy the code

Let’s print the result on the console and see that it matches our expectations.

3.2 Processing of functions

I’ll just return it. I don’t have to deal with it. Too few copy functions are needed in practical application scenarios…

const copy = input= > {
  if (typeof input === 'function'| |! isObject(input))return input
}
Copy the code
3.3 For the processing of Symbol value as the attribute key

Because of the above… The in method cannot traverse the attribute key of Symbol, so:

const sym = Symbol('desc')
const obj = {
  [sym]: 'This is symbol value'
}
console.log(deepCopy(obj)) // {}, the copy result has no [sym] attribute
Copy the code

Here, we need to use two methods:

  • Object. GetOwnPropertySymbols () it returns an array of all Symbol attribute of the Object itself, including an enumerated attribute.

  • Object. The prototype. PropertyIsEnumerable () which returns a Boolean value that indicates whether the specified attributes can be enumerated.

const copy = input= > {
  // Nothing else
  for (let key in input) {
    // ...
  }

  // Handle attributes with Symbol values as attribute keys.
  const symbolArr = Object.getOwnPropertySymbols(input)
  if (symbolArr.length) {
    for (let i = 0, len = symbolArr.length; i < len; i++) {
      if (input.propertyIsEnumerable(symbolArr[i])) {
        const value = input[symbolArr[i]]
        output[symbolArr[i]] = copy(value)
      }
    }
  }

  // ...
}
Copy the code

Let’s copy the source object:

const source = {}
const sym1 = Symbol('1')
const sym2 = Symbol('2')
Object.defineProperties(source,
  {
    [sym1]: {
      value: 'This is symbol value.'.enumerable: true
    },
    [sym2]: {
      value: 'This is a non-enumerable property.'.enumerable: false}})Copy the code

Print the result, also matching the expected result:

3.4 Processing Date Objects

In fact, the processing of Date objects is similar to the processing of wrapped objects mentioned above. For now, let’s put it in isWrapperObject() and handleWrapperObject().

const deepCopy = source= > {
  // Other unchanged...

  // Check whether it is a wrapped object.
  const isWrapperObject = obj= > {
    const theClass = getClass(obj)
    const type = /^\[object (.*)\]$/.exec(theClass)[1]
    return ['Boolean'.'Number'.'String'.'Symbol'.'BigInt'.'Date'].includes(type)
  }

  // Process the wrapped object
  const handleWrapperObject = obj= > {
    const type = getClass(obj)
    switch (type) {
      // Other cases remain unchanged
      // ...
      case '[object Date]':
        return new Date(obj.valueOf()) // new Date(+obj)
      default:
        return undefined}}// Other unchanged...
}
Copy the code
3.5 Processing Map and Set Objects

Again, we’ll put it in isWrapperObject() and handleWrapperObject() for now.

Use Map, Set object Iterator features and their own methods, can be quickly solved.

const deepCopy = source= > {
  // Other unchanged...

  // Check whether it is a wrapped object.
  const isWrapperObject = obj= > {
    const theClass = getClass(obj)
    const type = /^\[object (.*)\]$/.exec(theClass)[1]
    return ['Boolean'.'Number'.'String'.'Symbol'.'BigInt'.'Date'.'Map'.'Set'].includes(type)
  }

  // Process the wrapped object
  const handleWrapperObject = obj= > {
    const type = getClass(obj)
    switch (type) {
      // Other cases remain unchanged
      // ...
      case '[object Map]': {
        const map = new Map()
        obj.forEach((item, key) = > {
          // It is important to note that the key cannot be deeply copied, otherwise the reference will be lost
          // The specific reasons can be considered, not difficult. Leave it in the comments section
          map.set(key, copy(item))
        })
        return map
      }
      case '[object Set]': {
        const set = new Set()
        obj.forEach(item= > {
          set.add(copy(item))
        })
        return set
      }
      default:
        return undefined}}// Other unchanged...
}
Copy the code

Print the result:

3.6 Problems with circular References

The following is a circular reference object:

const foo = { name: 'Frankie' }
foo.bar = foo
Copy the code

Json.stringify () doesn’t handle circular references, so we’ll print it on the console:

As you can see from the results, when serializing a looping reference object, a TypeError is thrown: Uncaught TypeError: Converting circular structure to JSON.

Next, use the self-implemented deepCopy() method to see what the result is:

We see that a stack overflow occurs when the foo object referenced in the loop is copied.

In another article, I mentioned that you can handle circular references with jSON-js by first introducing the cycli.js script and then json.stringify (json.decycle (foo)). But fundamentally, it uses WeakMap to deal with.

Let’s implement it:

const deepCopy = source= > {
  // Create a WeakMap object to record the copied objects (this time added)
  const weakmap = new WeakMap(a)// Keep the middle block unchanged, omit ten thousand words...

  // Copy (recursive)
  const copy = input= > {
    if (typeof input === 'function'| |! isObject(input))return input

    // Return the object that has been copied directly (this time added to solve the problem of circular reference)
    if (weakmap.has(input)) {
      return weakmap.get(input)
    }

    // Process the wrapped object
    if (isWrapperObject(input)) {
      return handleWrapperObject(input)
    }

    const output = isArray(input) ? [] : {}

    // Record the objects copied each time
    weakmap.set(input, output)

    for (let key in input) {
      if (input.hasOwnProperty(key)) {
        const value = input[key]
        output[key] = copy(value)
      }
    }

    // Process attributes with Symbol values as attribute keys
    const symbolArr = Object.getOwnPropertySymbols(input)
    if (symbolArr.length) {
      for (let i = 0, len = symbolArr.length; i < len; i++) {
        if (input.propertyIsEnumerable(symbolArr[i])) {
          output[symbolArr[i]] = input[symbolArr[i]]
        }
      }
    }

    return output
  }

  return copy(source)
}
Copy the code

Take a look at the print, it won’t overflow like before.

It should be noted that the reason why WeakMap is not used here is as follows:

First, Map’s key is a strong reference, while WeakMap’s key is a weak reference. And the key of WeakMap must be an object, and the value of WeakMap is arbitrary.

Because of their key-value reference relationships, maps cannot ensure that objects referenced by them are not referenced by the garbage collector. Given that the Map we use, foo in the graph and the Map created by const Map = new Map() in our deep copy are always strongly referenced, foo will not be reclaimed and its memory will not be freed until the end of the program.

In contrast, the native WeakMap holds a “weak reference” to each key object, meaning garbage collection works correctly when no other reference exists. The structure of the native WeakMap is special and efficient, and the key it uses for mapping is valid only if it is not reclaimed.

Basically, if you want to add data to an object and don’t want to interfere with garbage collection, you can use WeakMap.

To see according to WeakMap?

The well-known deep-copy method of the Lodash library self-implements a constructor similar to WeakMap to handle circular references. (see attached)

Here’s another idea, which is also possible.

const deepCopy = source= > {
  // Otherwise, omit ten thousand words...

  // Create an array to copy each object into
  const copiedArr = []

  // Copy (recursive)
  const copy = input= > {
    if (typeof input === 'function'| |! isObject(input))return input

    // If there is a copy of the object, then directly put back, to solve the problem of circular reference
    for (let i = 0, len = copiedArr.length; i < len; i++) {
      if (input === copiedArr[i].key) return copiedArr[i].value
    }

    // Process the wrapped object
    if (isWrapperObject(input)) {
      return handleWrapperObject(input)
    }

    const output = isArray(input) ? [] : {}

    // Record the object each time
    copiedArr.push({ key: input, value: output })

    // The following process remains unchanged...
  }

  return copy(source)
}
Copy the code

There was a bug in the previous implementation. Thank you for pointing it out and now it has been corrected.

Test the following example after implementing deep copy:

const foo = { name: 'Frankie' }
foo.bar = foo

const cloneObj = deepCopy(foo) // Self-implementing deep copy
const lodashObj = _.cloneDeep(foo) // Lodash deep copy

// If the output is as follows, it is correct
console.log(lodashObj.bar === lodashObj) // true
console.log(lodashObj.bar === foo) // false
console.log(cloneObj.bar === cloneObj) // true
console.log(cloneObj.bar === foo) // false
Copy the code
3.7 Regular expression processing

There are two very important properties in regular expressions:

  • RegExp. Prototype. source returns a string of pattern text for the current regular expression object. Note that this is a new property in ES6.
  • RegExp. Prototype. flags Returns the current regular expression object flag.
const { source, flags } = /\d/g
console.log(source) // "\\d"
console.log(flags) // "g"
Copy the code

With these two attributes, we can use the new RegExp(Pattern, flags) constructor to create a regular expression.

const { source, flags } = /\d/g
const newRegex = new RegExp(source, flags) // /\d/g
Copy the code

Note, however, that the regular expression has a lastIndex property, which is readable and writable, and whose value is an integer that specifies the starting index for the next match. JavaScript RegExp objects are stateful when the global or sticky flag bits are set (e.g. /foo/g, /foo/y). They record the position since the last successful match in the lastIndex property.

Therefore, the above way of copying regular expressions is flawed. See the sample:

const re1 = /foo*/g
const str = 'table football, foosball'
let arr

while((arr = re1.exec(str)) ! = =null) {
  console.log(`Found ${arr[0]}. Next starts at ${re1.lastIndex}. `)}// The above statement outputs the following result:
// "Found foo. Next starts at 9."
// "Found foo. Next starts at 19."


// When we modify the lastIndex property of re1, output the following result:
re1.lastIndex = 9
while((arr = re1.exec(str)) ! = =null) {
  console.log(`Found ${arr[0]}. Next starts at ${re1.lastIndex}. `)}// "Found foo. Next starts at 19."

// I'm sure you all know the above.
Copy the code

As a result, you can see in the following example that the printable results are inconsistent because when the RegExp constructor is used to create a regular expression, lastIndex is set to 0 by default.

const re1 = /foo*/g const str = 'table football, Const re2 = new RegExp(re1.source, const re2 = new RegExp(re1.source, const re1) re1.flags) console.log('re1:') while ((arr = re1.exec(str)) ! == null) { console.log(`Found ${arr[0]}. Next starts at ${re1.lastIndex}.`) } console.log('re2:') while ((arr = re2.exec(str)) ! == null) { console.log(`Found ${arr[0]}. Next starts at ${re2.lastIndex}.`) } // re1: // expected output: "Found foo. Next starts at 19." // re2: // expected output: "Found foo. Next starts at 9." // expected output: "Found foo. Next starts at 19."Copy the code

Therefore:

const deepCopy = source= > {
  // other unchanged, omit...

  // Process regular expressions
  const handleRegExp = regex= > {
    const { source, flags, lastIndex } = regex
    const re = new RegExp(source, flags)
    re.lastIndex = lastIndex
    return re
  }

  // Copy (recursive)
  const copy = input= > {
    if (typeof input === 'function'| |! isObject(input))return input

    // Regular expressions
    if (getClass(input) === '[object RegExp]') {
      return handleRegExp(input)
    }

    //...
  }

  return copy(source)
}
Copy the code

The print results were as expected:

Since RegExp. Prototype. flags is a new property in ES6, let’s take a look at how ES5 implements it (from Lodash) :

/** Used to match `RegExp` flags from their coerced string values. */
var reFlags = /\w*$/;

/**
 * Creates a clone of `regexp`.
 *
 * @private
 * @param {Object} regexp The regexp to clone.
 * @returns {Object} Returns the cloned regexp.
 */
function cloneRegExp(regexp) {
  var result = new regexp.constructor(regexp.source, reFlags.exec(regexp));
  result.lastIndex = regexp.lastIndex;
  return result;
}
Copy the code

But again, it’s 2021, so let Babel handle ES5 compatibility.

3.8 Processing prototypes

Note that only prototype copies of objects of type “[object object]” are implemented. Arrays, for example, are not handled because there are too few actual scenarios for these cases.

This is mainly to modify the following step:

const output = isArray(input) ? [] : {}
Copy the code

Use object.create () to create an output Object.

const initCloneObject = obj= > {
  // Handle instance objects based on Object.create(null) or Object.create(Object.prototype.__proto__)
  // Object.prototype.__proto__ is the man on top of the prototype
  // But I noticed that the Lodash library's Clone method does not handle both cases
  if (obj.constructor === undefined) {
    return Object.create(null)}// Handle instance objects of custom constructors
  if (typeof obj.constructor === 'function'&& (obj ! == obj.constructor || obj ! = =Object.prototype)) {
    const proto = Object.getPrototypeOf(obj)
    return Object.create(proto)
  }

  return{}}const output = isArray(input) ? [] : initCloneObject(input)
Copy the code

If you look at the print, you can see that the source prototype object has been copied:

Consider the case of Object.create(null), which is also expected.

We can see that Lodash’s _.clonedeep (Object.create(null)) deep-copy method does not handle this case. Of course, copying this data structure in real life is really rare…

I found a related Issue #588 about why the Lodash copy method doesn’t implement this:

A shallow clone won’t do that as it’s just _.assign({}, object) and a deep clone is loosely based on the structured cloning algorithm and doesn’t attempt to clone inheritance or lack thereof.

Four, optimization

In summary, the complete but not optimized deep-copy method is as follows:

const deepCopy = source= > {
  // Create a WeakMap object to record the copied objects
  const weakmap = new WeakMap(a)// Get the data type
  const getClass = x= > Object.prototype.toString.call(x)

  // Check whether it is an array
  const isArray = arr= > getClass(arr) === '[object Array]'

  // Check whether it is a reference type
  const isObject = obj= >obj ! = =null && (typeof obj === 'object' || typeof obj === 'function')

  // Check whether it is a wrapper object
  const isWrapperObject = obj= > {
    const theClass = getClass(obj)
    const type = /^\[object (.*)\]$/.exec(theClass)[1]
    return ['Boolean'.'Number'.'String'.'Symbol'.'BigInt'.'Date'.'Map'.'Set'].includes(type)
  }

  // Process the wrapped object
  const handleWrapperObject = obj= > {
    const type = getClass(obj)
    switch (type) {
      case '[object Boolean]':
        return Object(Boolean.prototype.valueOf.call(obj))
      case '[object Number]':
        return Object(Number.prototype.valueOf.call(obj))
      case '[object String]':
        return Object(String.prototype.valueOf.call(obj))
      case '[object Symbol]':
        return Object(Symbol.prototype.valueOf.call(obj))
      case '[object BigInt]':
        return Object(BigInt.prototype.valueOf.call(obj))
      case '[object Date]':
        return new Date(obj.valueOf()) // new Date(+obj)
      case '[object Map]': {
        const map = new Map()
        obj.forEach((item, key) = > {
          map.set(key, copy(item))
        })
        return map
      }
      case '[object Set]': {
        const set = new Set()
        obj.forEach(item= > {
          set.add(copy(item))
        })
        return set
      }
      default:
        return undefined}}// Process regular expressions
  const handleRegExp = regex= > {
    const { source, flags, lastIndex } = regex
    const re = new RegExp(source, flags)
    re.lastIndex = lastIndex
    return re
  }

  const initCloneObject = obj= > {
    if (obj.constructor === undefined) {
      return Object.create(null)}if (typeof obj.constructor === 'function'&& (obj ! == obj.constructor || obj ! = =Object.prototype)) {
      const proto = Object.getPrototypeOf(obj)
      return Object.create(proto)
    }

    return{}}// Copy (recursive)
  const copy = input= > {
    if (typeof input === 'function'| |! isObject(input))return input

    // Regular expressions
    if (getClass(input) === '[object RegExp]') {
      return handleRegExp(input)
    }

    // If the object has been copied, return it directly (solve the problem of circular reference)
    if (weakmap.has(input)) {
      return weakmap.get(input)
    }

    // Process the wrapped object
    if (isWrapperObject(input)) {
      return handleWrapperObject(input)
    }

    const output = isArray(input) ? [] : initCloneObject(input)

    // Record the objects copied each time
    weakmap.set(input, output)

    for (let key in input) {
      if (input.hasOwnProperty(key)) {
        const value = input[key]
        output[key] = copy(value)
      }
    }

    // Process attributes with Symbol values as attribute keys
    const symbolArr = Object.getOwnPropertySymbols(input)
    if (symbolArr.length) {
      for (let i = 0, len = symbolArr.length; i < len; i++) {
        if (input.propertyIsEnumerable(symbolArr[i])) {
          const value = input[symbolArr[i]]
          output[symbolArr[i]] = copy(value)
        }
      }
    }

    return output
  }

  return copy(source)
}
Copy the code

The next step is optimization…

4.1 to optimize a

We used for… And Object in getOwnPropertySymbols () method to traverse the Object’s properties (including string and Symbol attribute), also involves an enumerable and an enumerated attribute.

  • for… In: Iterates over itself and inherited enumerable properties (excluding Symbol properties).
  • Keys: Returns an array containing all the enumerable properties of the Object itself (excluding the non-enumerable and Symbol properties)
  • Object. GetOwnPropertyNames: returns an array that contains the attributes of the Object itself (including an enumeration attributes, but does not include Symbol attribute)
  • Object. GetOwnPropertySymbols: returns an array containing all the Symbol attribute of the Object itself (including an enumerable and an enumerated attribute)
  • Reflect.ownkeys: Returns an array containing all of its properties (including Symbol, non-enumerable, and enumerable properties)

Because we can just copy the enumeration of the string and the Symbol can be enumerated attribute, so we will Reflect. OwnKeys () and Object. The prototype. PropertyIsEnumerable () can be used in combination.

So, we will make the following part:

for (let key in input) {
  if (input.hasOwnProperty(key)) {
    const value = input[key]
    output[key] = copy(value)
  }
}

// Process attributes with Symbol values as attribute keys
const symbolArr = Object.getOwnPropertySymbols(input)
if (symbolArr.length) {
  for (let i = 0, len = symbolArr.length; i < len; i++) {
    if (input.propertyIsEnumerable(symbolArr[i])) {
      const value = input[symbolArr[i]]
      output[symbolArr[i]] = copy(value)
    }
  }
}
Copy the code

Optimized:

// Only iterate over the enumerable attributes of the object itself (including string attributes and Symbol attributes)
Reflect.ownKeys(input).forEach(key= > {
  if (input.propertyIsEnumerable(key)) {
    output[key] = copy(input[key])
  }
})
Copy the code
4.2 optimization of two

Optimize getClass(), isWrapperObject(), handleWrapperObject(), handleRegExp() and their related type determination methods.

HandleWrapperObject () is intended to handle wrapped objects, but as more and more special objects are to be handled later, it is a little confusing to write them in order to reduce the length of the article.

So let’s integrate it a little bit. Some of the handlers may change their names.

Fifth, in the end

In fact, some of the boundary cases mentioned above, or other special objects (such as arrayBuffers, etc.), are not dealt with here, but I think it’s time to end because there are too few of them in practice.

The code has been thrown to GitHub 👉 toFrankie/ some-javascript-file.

The same words:

If used in production environmentJSON.stringify()Unable to solve your needs, please useLodashThe library_.cloneDeep()Method? That’s what I call everything. Do not use my method, remember!

This article is geared towards learning, interviewing (manual head), and perhaps familiarizing yourself with some of the characteristics of the object. If there is insufficient, welcome to point out, thank you 👋 ~

Finally…… To finish, vomited three pounds of blood…

Final version is as follows:

const deepCopy = source= > {
  // Create a WeakMap object to record the copied objects
  const weakmap = new WeakMap(a)// Get the data type, return values such as "Object", "Array", "Symbol", etc
  const getClass = x= > {
    const type = Object.prototype.toString.call(x)
    return /^\[object (.*)\]$/.exec(type)[1]}// Check whether it is an array
  const isArray = arr= > getClass(arr) === 'Array'

  // Check whether it is a reference type
  const isObject = obj= >obj ! = =null && (typeof obj === 'object' || typeof obj === 'function')

  // Determine if it is a "special" object (requires special treatment)
  const isSepcialObject = obj= > {
    const type = getClass(obj)
    return ['Boolean'.'Number'.'String'.'Symbol'.'BigInt'.'Date'.'Map'.'Set'.'RegExp'].includes(type)
  }

  // Handle special objects
  const handleSepcialObject = obj= > {
    const type = getClass(obj)
    const Ctor = obj.constructor // The object's constructor
    const primitiveValue = obj.valueOf() // Get the original value of the object

    switch (type) {
      case 'Boolean':
      case 'Number':
      case 'String':
      case 'Symbol':
      case 'BigInt':
        // Handle the Wrapper Object
        return Object(primitiveValue)
      case 'Date':
        return new Ctor(primitiveValue) // new Date(+obj)
      case 'RegExp': {
        const { source, flags, lastIndex } = obj
        const re = new RegExp(source, flags)
        re.lastIndex = lastIndex
        return re
      }
      case 'Map': {
        const map = new Ctor()
        obj.forEach((item, key) = > {
          // Note that even if the Map object's key is of reference type, copy(key) cannot be used. Otherwise, the reference will be lost and the property cannot be accessed.
          map.set(key, copy(item))
        })
        return map
      }
      case 'Set': {
        const set = new Ctor()
        obj.forEach(item= > {
          set.add(copy(item))
        })
        return set
      }
      default:
        return undefined}}// Create the output object (prototype copy is the key step)
  const initCloneObject = obj= > {
    if (obj.constructor === undefined) {
      return Object.create(null)}if (typeof obj.constructor === 'function'&& (obj ! == obj.constructor || obj ! = =Object.prototype)) {
      const proto = Object.getPrototypeOf(obj)
      return Object.create(proto)
    }

    return{}}// Copy method (recursive)
  const copy = input= > {
    if (typeof input === 'function'| |! isObject(input))return input

    // If the object has been copied, return it directly (solve the problem of circular reference)
    if (weakmap.has(input)) {
      return weakmap.get(input)
    }

    // Process the wrapped object
    if (isSepcialObject(input)) {
      return handleSepcialObject(input)
    }

    // Create an output object
    const output = isArray(input) ? [] : initCloneObject(input)

    // Record the objects copied each time
    weakmap.set(input, output)

    // Only iterate over the enumerable attributes of the object itself (including string attributes and Symbol attributes)
    Reflect.ownKeys(input).forEach(key= > {
      if (input.propertyIsEnumerable(key)) {
        output[key] = copy(input[key])
      }
    })

    return output
  }

  return copy(source)
}
Copy the code

Six, reference

  • GitHub/lodash
  • Are you really familiar with JavaScript?

The end.