preface

Why do JS object assignments sometimes need to be deep-copied?


First of all, js data values are mainly divided into two categories according to their types.
Basic data typesand
Reference data type. Basic data types include Undefined, Null, Number, String, Boolean, Symbol; The reference data type is Object, and the Array, Set, and Map data also belong to Object.


And then, let’s look at a very typical example

// Base data type assignment
var a = 'aaa';
var b = a;
console.log(a);  // 'aaa'
console.log(b);  // 'aaa'
b = 'bbb';
console.log(a);  // 'aaa'
console.log(b);  // 'bbb'

// Reference data type assignment
var a = {    name: 'Joe'};
var b = a;
console.log(a);  // {name: ""}
console.log(b);  // {name: ""}
b.name = 'bill';
b.age = 18;
console.log(a);  // {name: "li ", age: 18}
console.log(b);  // {name: "li ", age: 18}Copy the code

  


As you can see from the above code and figure, the assignment of the base data type is actually a copy, and there is no relationship between the values identified by the two variables. And created a reference data after the type of object, the object is stored in memory, as is the object the variable a points to address in memory, then the variable assignment to a variable b, is the a and b points to the same object, so either modify variables a or b, they will output the same content.

So if we assign to an object and we don’t want to share the object, then we need to make a deep copy, just like a copy of the primitive data type.

Deep copy method

1. Json.parse (json.stringify ()) serialization and antisequence

First, the object to be copied is JSON string-like, and then pase is parsed out and assigned to another variable to achieve deep copy.

There are some drawbacks to this approach, and I’m starting to show them with some examples.

// Test data vartest = {  name: "test"};
var data = {  a: "123", 
              b: 123, 
              c: true, 
              d: [43, 2], 
              e: undefined,
              f: null,
              g: function() {    console.log("g");  },
              h: new Set([3, 2, null]),
              i: Symbol("fsd"),
              j: test,
              k: new Map([    ["name"."Zhang"],    ["title"."Author"]])}; JSON.stringify(data)Copy the code

The execution result



Can see the data in the object’s properties basically contains all data types, but through after the JSON string, the return value is missing, the reason is that JSON when performing string of this process, will be a JSON format, access to safe JSON values, so if a secure JSON value, will be discarded. The values of undefined, function and symbol are unsafe (including the property of the object which is cyclically assigned to the object), so they are filtered out after formatting, and the data format objects such as set and map are not properly processed, but are treated as empty objects.

Let’s take another extreme example

Var data = {name:'foo',
    child: null,
}
data.child = data
Copy the code



Cyclic references to the properties of this object form a closed loop, perform the serialization, and see the result



As you can see, the JSON serialization of an object with a closed loop popped an error

So when using JSON serialization, be careful not to include any of the above data types, but there are a few advantages to this method, so let’s take a look at the examples.

// Test data vartest = {  name: "test"};
var data = {  a: "123",
              b: 123,
              c: true,
              d: [43, 2],
              e: test,
              f: { 
                    name: 'Joe',
                    age: 18, 
                    likes: {         
                         ball: ['football'.'basketball']}}}; JSON.stringify(data)Copy the code

The execution result



It has the advantage of handling nested objects or property values that are references to another object without losing data.

The specific methods

function deepCopy(obj){ 
   if(typeof obj === 'function'){   
     throw new TypeError('Please pass in the correct data type format')
    }
    try {
        let data = JSON.stringify(obj)
        let newData = JSON.parse(data)
        return newData
     } catch(e) {
      console.log(e)
      }
}Copy the code

2. Object.assign(target, source1, source2)

A new method in ES6 can be used for object merging to copy all the enumerable properties of the source object to the target object.

var data = {
              a: "123",
              b: 123,
              c: true,
              d: [43, 2],
              e: undefined,
              f: null,
              g: function() {    console.log("g");  },
              h: new Set([3, 2, null]),
              i: Symbol("fsd"),
              k: new Map([    ["name"."Zhang"],    ["title"."Author"]])}; var newData = Object.assign({},data) console.log(newData)Copy the code

The execution result



You can see that the API copies all the data type attribute values from the source object into a new object. Is this the perfect deep copy we are looking for? The answer is, is it only a partial deep copy, or is it a shallow copy, why is that? Let’s move on.

var test = {  name: 'Joe' }
var data = { 
              a: 123,
              b: test
            }
var newData = Object.assign({},data)
console.log(newData) 
// {  a: 123,  b: {    name: 'Joe'  }}
test.age = 18
console.log(newData)
// {  a: 123,  b: {    name: 'Joe',   age: 18  }}Copy the code

It turns out that copying in this way, if the value of an attribute in the source target object is a reference to another object, the copy of that attribute is still a copy of the reference.

3. Iterative recursive methods

Without saying more, first attach their own code

function deepCopy(data) {
      if(typeof data ! = ='object' || data === null){
            throw new TypeError('Passed argument is not an object')}letnewData = {}; const dataKeys = Object.keys(data); dataKeys.forEach(value => { const currentDataValue = data[value]; // Copy the values and functions directly assigned to the basic data typesif(typeof currentDataValue ! = ="object" || currentDataValue === null) {
              newData[value] = currentDataValue;
          } else if(array.isarray (currentDataValue)) {// Implement a deep copy of Array newData[value] = [...currentDataValue]; }else ifCurrentDataValue Instanceof Set {// implementsetNewData [value] = new Set([...currentDataValue]); }else if(currentDataValue instanceof Map) {newData[value] = new Map([...currentDataValue]); }elseNewData [value] = deepCopy(currentDataValue); }});return newData;
  }Copy the code

Then write a test data to actually test it

Var data = {age: 18, name:"liuruchao",
              education: ["Primary school"."Junior"."High school"."University", undefined, null],
              likesFood: new Set(["fish"."banana"]),
              friends: [
                    { name: "summer",  sex: "woman"},
                    { name: "daWen",   sex: "woman"},
                    { name: "yang",    sex: "man" }  ], 
              work: { 
                      time: "2019", 
                      project: { name: "test",obtain: ["css"."html"."js"]} 
                    }, 
              play: function() {    console.log("Skateboarding"); }}Copy the code


The execution result



Basically can satisfy the common data structure of the value of the deep copy, but because of the js object more data structure, so not all covered, such as new Number(), the basic data type of the wrapper object, is not processed. Therefore, when using it, you can first make a prediction of the object to be deep-copied to determine which method to use.

Again, try this extreme example of a closed loop object in the first method

Var data = {name:'foo',
    child: null,
}
data.child = data

deepCopy(data)
Copy the code



Oh, direct stack overflow, stack burst!! The recursion method should be used with great care, because it can be pushed too much if you are not careful, so you can only optimize this function.

4. Iterative recursive method (solving closed-loop problems)

function deepCopy(data, hash = new WeakMap()) {
      if(typeof data ! = ='object' || data === null){
            throw new TypeError('Passed argument is not an object'} // Determine if the passed reference to the object to be copied existshashIn theif(hash.has(data)) {
            return hash.get(data)
        }
      letnewData = {}; const dataKeys = Object.keys(data); dataKeys.forEach(value => { const currentDataValue = data[value]; // Copy the values and functions directly assigned to the basic data typesif(typeof currentDataValue ! = ="object" || currentDataValue === null) {
              newData[value] = currentDataValue;
          } else if(array.isarray (currentDataValue)) {// Implement a deep copy of Array newData[value] = [...currentDataValue]; }else ifCurrentDataValue Instanceof Set {// implementsetNewData [value] = new Set([...currentDataValue]); }else if(currentDataValue instanceof Map) {newData[value] = new Map([...currentDataValue]); }else{// Store a reference to the object to be copied inhashNewData [value] = deepCopy(currentDataValue,hash); }});return newData;
  }Copy the code

There is more container WeakMap for storing objects than the previous version 1.0. The idea is that when deepCopy is called for the first time, the parameter will create a WeakMap object. One of the characteristics of this data structure is that the keys in the stored key-value pair must be object types.

  1. When called for the first time, weakMap is empty, and the above if(hash.has()) statement will not be used. If the object to be copied has properties that are also objects, the object to be copied will be stored in weakMap. At this time, the key value and key name are references to the object to be copied
  2. The function is then called recursively
  3. Enter the function again, pass in the object attribute reference of the last object to be copied and store the weakMap of the last object to be copied, because if it is a closed loop generated by circular reference, then the two references point to the same object, so it will enter if(hash.has()) statement, and then return, exit the function, So you don’t keep recursively pushing the stack, so you don’t overflow the stack.

conclusion

Regardless of their advantages and disadvantages, the common point is that only enumerable properties of objects can be copied, but not properties that are not enumerable or prototypical, which is sufficient for basic use. Finally, if there are mistakes or omissions in the article or you have better suggestions, welcome to communicate with me


If this article has so a little help to you, please help to point a praise oh, is also a pair of a kind of encouragement 😀!