preface

Deep copy is used when we need to manipulate a copy of an object or array without affecting the original data.

Deep-copy functional programming is common because it encourages us to use pure functions (the same input always gets the same output), so we need to make a deep copy of the application type data passed, and then manipulate the deep-copy data.

Similarly in jQuery, when we use $.extend() to extend attributes to a normal object, the default is shallow copy, or we can manually specify deep copy.

As you can see, deep copying is very common in development, so we must learn how to make deep copies of a source object and consider the problems that arise at every step, such as copying objects with circular references.

A way to achieve deep copy

There are two ways to implement deep copy in javascript:

  • JSON.parse()JSON.stringify()
  • A recursive algorithm

The two methods are explained and compared below.

JSON.parse()JSON.stringify()

Json.stringify () converts the value to the JSON format of the response, and json.parse () makes a deep copy of the object by parsing the JSON string. The code is shown as follows:

const person = {
   name: 'seven'.info: {
      age: 20}}const person1 = JSON.parse(JSON.stringify(person))
person.info.age = 18
person1.name = 'juejin'
console.log(person) // { name: 'seven', info: { age: 18 } }
console.log(person1) // { name: 'juejin', info: { age: 20 } }
Copy the code

As you can see, the copied Person1 has nothing to do with the source object Person, and it doesn’t affect each other when they change the property values, so this makes deep copy.

Existing problems

This deep-copy approach is easy to implement, but there are a number of problems that need to be addressed before exploring: json.stringify ()

  • undefined,Arbitrary functionAs well assymbolValue will be used during serializationignore(when present in an attribute value of a non-array object) or converted to NULL (when present in an array).function,undefinedReturns when converted separatelyundefined, such asJSON.stringify(function(){})orJSON.stringify(undefined)
  • Executing this method on objects that contain circular references (objects that refer to each other in an infinite loop) throws an error
  • .

More on the rules for converting json.stringify () to the appropriate JSON format can be found in the MDN documentation.

The two things listed above are the most important and the most common situations in practice, which can be explained very well:

  • JSONIs essentially a lightweight data interchange format used for front – and back-end interactionsTo transmit data, so other high-level languages such asjava.pythonAnd so on have to be resolvable, so forjavascriptIn theundefined, functions,symbolType of data that cannot be parsed by other languages and therefore causes an error.
  • For objects with circular references, the nesting level is infinitely deep, so serialization can go into dead-loop or dead-recursion.

The recursive implementation

Parse () and json.stringify () have been covered in deep copy, and you can see that there are a lot of drawbacks, but it can be used directly if you make it “safe”. After all, it only takes one line of code.

Then we’ll look at deep copies of recursive implementations and see what we need to do:

  • For attribute values of primitive data types, just copy the assignment.
  • For attribute values of complex data types, direct copy assignments will get the reference of the value, no deep copy is implemented. So we’re going to recursively evaluate objects of complex types.

Let’s start with a simple version of deep-copy code:

const deepClone = (obj) = > {
   Return {} if the source object is not an object
   if(! obj ||typeofobj ! = ='object') {
      return{}}// New copy of data, consider arrays and objects
   const newObj = Array.isArray(obj) ? [] : {}
   // Loop over subscripts or properties of arrays or objects
   for (const key in obj) {
      const value = obj[key]
      // Deep copy recursively if the attribute value is an object, otherwise assign directly
      newObj[key] = typeof value === 'object' ? deepClone(value) : value
   }
   return newObj
}

const person = {
   name: 'seven'.info: {
      age: 20}}const person1 = deepClone(person)
person1.name = 'juejin'
person.info.age = 18
console.log(person) // { name: 'seven', info: { age: 18 } }
console.log(person1) // { name: 'juejin', info: { age: 20 } }
Copy the code

I believe that the above code is very simple to implement, as you can see that it is also possible to implement a deep copy of the object.

Existing problems

Parse () and json.stringify () can copy undefined, function, and symbol values.

For looping objects, we can try:

const seven = {
   name: 'seven'
}
const juejin = {
   name: 'juejin'.relative: seven
}
seven.relative = juejin
const newObj = deepClone(seven)
console.log(newObj)
Copy the code

Error: code to run directly after Maximum call stack size exceeded, the call stack overflow, because we need to copy the source of circular reference objects exist, so deepClone function will continue into the stack, stack overflow in the end.

The solution to this circular reference problem is simple: we simply save the value before each deep copy of a complex data type, and if the value occurs again, we stop copying and cut it off.

Here, we choose WeakMap or Map data structure in ES6 to store the value of each complex type. We also encapsulate the internal logic of the original deepClone function into another internal function, in order to define mappings outside the internal function and form closures:

You can refer to the documentation for the difference between WeakMap and Map.

const deepClone = (obj) = > {
   // Define a map that initializes and adds obj itself to the map
   const map = new WeakMap()
   map.set(obj, true)
   // Encapsulate the original recursive logic
   const copy = (obj) = > {
      if(! obj ||typeofobj ! = ='object') {
         return{}}const newObj = Array.isArray(obj) ? [] : {}
      for (const key in obj) {
         const value = obj[key]
         // Copy a value of a simple type
         if (typeofvalue ! = ='object') {
            newObj[key] = value
         } else {
         	// If a complex data type is copied for the first time, store it in map
            // The second time the value is encountered, it is directly assigned to null, ending the recursion
            if (map.has(value)) {
               newObj[key] = null
            } else {
               map.set(value, true)
               newObj[key] = copy(value)
            }
         }
      }
      return newObj
   }
   return copy(obj)
}

// test
const seven = {
   name: 'seven'
}
const juejin = {
   name: 'juejin'.relative: seven
}
seven.relative = juejin
const newObj = deepClone(seven)
console.log(newObj)
// { name: 'seven', relative: { name: 'juejin', relative: null } }
Copy the code

As you can see, this also enables deep copy and resolves the previous stack overflow exception caused by circular references.