Implement a deep copy

Parse (json.stringify (target)), or cloneDeep (LoDash). We rarely implement a deep copy ourselves. I know how to use recursion, but after trying it out, THERE are a lot of things to be aware of.

Implementation with JSON

This is easy to do in one line of code:

JSON.parse(JSON.stringify(target))
Copy the code

Functions and undefined, regular expressions, maps, sets, etc., will be ignored as empty objects: {}, and objects with circular references will be reported. But it’s handy if you’re sure the target object you’re copying doesn’t have these types.

Now let’s look at the second, recursive implementation

Simple version of recursion implementation

Since the value of an object can also be an object, we are not sure how many levels there are, so it is easy to think of using a recursive implementation:

function cloneDeep(target) {
  if (typeof target === 'object') {
    const cloneTarget = Array.isArray(target) ? [] : {}
    const keys = Object.keys(target)
    for(let i = 0; i < keys.length; i++) {
      cloneTarget[keys[i]] = cloneDeep(target[keys[i]])
    }
    return cloneTarget
  }
  return target
}
Copy the code

The above code is very simple, determine if the target is an Object, then determine whether it is an Object or Array, recursively iterate over each item, add the value to the new Object, and finally return the new Object, if the target is not an Object. Let’s try it:

const obj = { a: 1.b: 2.c: { d: 3.e: [4, { f: 5, : g: 6}}}]const cloneObj = cloneDeep(obj)
obj.c.e[1].f = '5'
console.log(cloneObj) // { a: 1, b: 2, c: { d: 3, e: [4, { f: 5, : g: 6 }] } }
Copy the code

Alter obj (obj, obj, obj, obj, obj, obj, obJ, obJ, obJ, obJ) Parse (json.stringify ()) does not solve the same problems that this version of cloneDeep does. Next, let’s solve a circular reference problem.

Versions that resolve circular references

Let’s start by looking at what happens when we copy the object referenced in the loop using the existing method:

const obj = {a: 1.b: 2 }
obj.obj = obj
cloneDeep(obj)
Copy the code

Error: Maximum Call Stack size exceeded

So how to solve it? If cloneDeep does not execute cloneDeep, it will return the value. If cloneDeep does not execute cloneDeep, it will return the value. The flow chart can be roughly expressed as follows:

For obj above, we expand it two layers like this:

{
  a: 1,
  b: 2,
  obj: {
    a: 1,
    b: 2, obj: {... }}}Copy the code

Make a deep copy of it and that’s it: Obj is an object => not copied => Create an empty object cloneTarget and save it (obj -> cloneTarget) => add the values of obj.a and obj.b to cloneTarget => obj.obj Obj === obj.obj) => Directly fetch assignment => exit the loop.

To use an object as a key, it is easy to think of using a map, so the code could be improved to look like this:

function cloneDeep(target, m = new Map(a)) {
  if (typeof target === 'object') {
    if (m.has(target)) {
      return m.get(target)
    }
    const cloneTarget = Array.isArray(target) ? [] : {}
    m.set(target, cloneTarget)
    const keys = Object.keys(target)
    for(let i = 0; i < keys.length; i++) {
      cloneTarget[keys[i]] = cloneDeep(target[keys[i]], m)
    }
    return cloneTarget
  }
  return target
}
Copy the code
const obj = {a: 1.b: 2 }
obj.obj = obj
cloneDeep(obj)
Copy the code

Try the circular copy again and you can see that the problem has been resolved. When typeof target === ‘object’ in JavaScript, there are not only objects and arrays, such as regular expressions, maps, sets, etc. In this case, our method above will return an empty object, so we need to make more detailed type judgments.

Add improved versions of other types of objects

Let’s first write a method to get a type:

const getType = target= > Object.prototype.toString.call(target).slice(8, -1)
Copy the code

When we call the getType method, we may return the following values:

"Number"."String"."Boolean"."Undefined"."Null"."Symbol"."BigInt"."Object"."Array"."RegExp"."Map"."Set"."Function"
Copy the code

Next we work with other types of objects

Regular expression processing

RegExp. Prototype. source: RegExp. Prototype. flags: RegExp. Prototype. flags: RegExp.

  const { source, flags } = reg
  cloneTarget = new RegExp(source, flags)
Copy the code

Map and Set processing

For a Map, we just create a new Map and recursively process it

const cloneTarget = new Map()
target.forEach((value, key) = > {
  cloneTarget.add(key, cloneDeep(value, m))
})
Copy the code

On the Set:

const cloneTarget = new Set()
target.forEach(value= > {
  cloneTarget.add(cloneDeep(value, m))
})
Copy the code

Handling of the Function type

For Function, we can call toString to convert the Function to a string, and then use eval to get a copy of the Function as follows:

const funcString = target.toString()
const cloneTarget = eval(` (() = >${funcString}`) ())
cloneTarget.prototype = target.prototype // The prototype points to the prototype of the original function
Copy the code

After adding the above types of processing, our final version is as follows:

const getType = target= > Object.prototype.toString.call(target).slice(8, -1)

function cloneDeep(target, m = new Map(a)) {
  const targetType = getType(target)
  let cloneTarget
  switch (targetType) {
    case 'Number':
    case 'String':
    case 'Boolean':
    case 'Undefined':
    case 'Null':
    case 'Symbol':
    case 'BigInt':
      return target
    case 'Object':
    case 'Array':
      // The constructor merges the case of objects and arrays
      cloneTarget = new target.constructor()
      break
    case 'RegExp':
      const { source, flags } = target
      cloneTarget = new RegExp(source, flags)
      break
    case 'Map':
      cloneTarget = new Map()
      target.forEach((value, key) = > {
        cloneTarget.set(key, cloneDeep(value, m))
      })
      break
    case 'Set':
      cloneTarget = new Set()
      target.forEach(value= > {
        cloneTarget.add(cloneDeep(value, m))
      })
      break;
    case 'Function':
      const funcString = target.toString()
      cloneTarget = eval(` (() = >${funcString}`) ())
      cloneTarget.prototype = target.prototype // The prototype points to the prototype of the original function
      break
    default:
      return target
  }
  if (cloneTarget) {
    if (m.has(target)) {
      return m.get(target)
    }
    m.set(target, cloneTarget)
    const keys = Object.keys(target)
    for(let i = 0; i < keys.length; i++) {
      cloneTarget[keys[i]] = cloneDeep(target[keys[i]], m)
    }
    return cloneTarget
  }
}
Copy the code

The last

So far we have implemented a deep-copy function that handles circular references, function copies, and copies of maps, sets, and regular expressions.

If there are any mistakes, please point them out.