js advanced deep and shallow copy explanation and es5 es6 implementation of circular reference and deep copy of symbol type data

The copy of objects in js is very important. If you don't understand it deeply, you will write fatal bug s. This article will summarize all the copy methods used in the previous project development.

The essential reason of object copy and the concept of shallow copy and deep copy

Due to the mechanism of js to create objects into memory, the original data types Undefined, Null, Boolean, Number and String are stored directly in the stack. Therefore, the assignment operation of these five types of data is to copy the result value directly in memory and save it, while the data of object type are all reference data types. The address reference is saved in the stack, and the actual value is stored in the heap, while the reference is saved in the stack The assignment operation of type is actually to copy the reference address in the stack, so if a variable with the same reference address modifies a property value of the object, it will cause the change of other objects with the same reference address, resulting in a deviation from the expected result.

For example:

   //Define an employee personal object
    let obj = {
        name:"cc",
        age:30,
        job:{type:"porgrammer",com:"ali"},
        cars:["passat","bmw"]
    }
    //This function only gets the employee's vehicle basic information name and vehicle
    let getPerInf = function(obj){
        var newObj = obj;
        if(newObj){
            if(newObj.age){
                delete newObj.age;
            }
            if(ewObj.job){
                delete newObj.job;
            }
        }
        return newObj;
    }

    //Print the newobj result and original obj result after function processing
    console.log(getPerInf(obj));
    console.log(obj);

Result:

Through the screenshot, we find that after the getperInf function is used to delete the job and age attributes, it also plays a role in the globally defined obj object. What should we do when adding other functions that need a complete obj?

So the above will inevitably lead to the problem of copying objects.

1.1 shallow copy

Compared with the concept of deep copy, the concept of shallow copy only assigns a value to the first attribute of the original object and generates a new object. Its essence is determined by the structure of the object, such as the obj object defined in the above example.

 let obj = {
        name:"cc",
        age:30,
        job:{type:"porgrammer",com:"ali"},
        cars:["passat","bmw"]
    }

In obj, there are original data type attributes name and age as well as reference data types job and cars. If shallow copy is used to realize the above requirements, it is as follows:

   //Define an employee personal object
    let obj = {
        name:"cc",
        age:30,
        job:{type:"porgrammer",com:"ali"},
        cars:["passat","bmw"]
    }
    //This function only gets the employee's vehicle basic information name and vehicle
    let getPerInf = function(obj){
        var newObj = shshallowClone(obj);
        if(newObj){
            if(newObj.age){
                delete newObj.age;
            }
            if(newObj.job){
                delete newObj.job;
            }
        }
        return newObj;
    }
    //Shallow copy es implementation

    let shshallowClone = function(obj){
        let newObj =new Object();
        if(obj){
             for(key in obj){
                 newObj[key] = obj[key];
             }
        }
        return newObj;
    }

    console.log(obj);
    console.log(getPerInf(obj));
    console.log(obj);

Result:

Through shallow copy, we find that getPerInf method achieves the desired data result and the global obj object is not affected.

However, there are still problems. If we change the com value of job object to "360" in getPerInf method, and then change the tyep value of job object of obj object to "manage" in global, the expected result is that the newObj object returned in getPerInf method will not be affected by the job.type modified by obj of the whole office, and the global obj object will not be affected by the job.com in newObj
Effect of modification:

    //Define an employee personal object
    let obj = {
        name:"cc",
        age:30,
        job:{type:"porgrammer",com:"ali"},
        cars:["passat","bmw"]
    }
    //This function only gets the employee's vehicle basic information name and vehicle
    let getPerInf = function(obj){
        var newObj = shshallowClone(obj);
        if(newObj){
            if(newObj.age){
                delete newObj.age;
            }
            if(newObj.job){
                // delete newObj.job;
                newObj.job.com = '360'
            }        
        }
        return newObj;
    }

    //Shallow copy es implementation

    let shshallowClone = function(obj){
        let newObj =new Object();
        if(obj){
             for(key in obj){
                 newObj[key] = obj[key];
             }
        }
        return newObj;
    }

    console.log(obj);
    console.log(getPerInf(obj));
    if(obj.job){
        obj.job.type = 'manager';
    }
    console.log(obj);

Result:

Unfortunately, the two are the same, which means that the reference of obj.job object is exactly the same as that of obj.job object.

To solve the above problems, we must do deep copy.

1.2 deep copy

As the name implies, deep copy is to copy all the properties of the original object, including the objects whose sub properties are reference properties, to the new object. The purpose is to completely block the address reference with the original object, so as to achieve no impact on each other.

     //Define an employee personal object
        let obj = {
            name:"cc",
            age:30,
            job:{type:"porgrammer",com:"ali"},
            cars:["passat","bmw"]
        }
        //This function only gets the employee's vehicle basic information name and vehicle
        let getPerInf = function(obj){
            // var newObj = shshallowClone(obj);
            let newObj = deepClone(obj)
            if(newObj){
                if(newObj.age){
                    delete newObj.age;
                }
                if(newObj.job){
                    // delete newObj.job;
                    newObj.job.com = '360'
                }        
            }
            return newObj;
        }

        //Deep copy

        let deepClone = function (obj){
            let newObj = (obj instanceof Array)? new Array() : new Object();
            if(obj){
                for(let key in obj){
                    if(typeof(obj[key]) === "object"){
                        newObj[key] = deepClone(obj[key]);
                    }else{
                        newObj[key] = obj[key];
                    }
                }  
            }
            return newObj;
        }
        console.log(obj);
        console.log(getPerInf(obj));
        if(obj.job){
            obj.job.type = 'manager';
        }
        console.log(obj);

Result:

In this way, we can completely copy all the attributes of the object, and any operation between obj and newObj will not be affected.

We have explained the reason and essence of object copying and the difference between shallow copy and deep copy in detail. Next, we will list some common implementation methods of deep copy and shallow copy of objects.

2 es5 native implementation object shallow and deep copy

2.1 shallow copy

 //Shallow copy es implementation

        let shshallowClone = function(obj){
            let newObj =new Object();
            if(obj){
                for(key in obj){
                    newObj[key] = obj[key];
                }
            }
            return newObj;
        }

2.2 deep copy

let deepClone = function (obj){
            let newObj = (obj instanceof Array)? new Array() : new Object();
            if(obj){
                for(let key in obj){
                    if(typeof(obj[key]) === "object"){
                        newObj[key] = deepClone(obj[key]);
                    }else{
                        newObj[key] = obj[key];
                    }
                }  
            }
            return newObj;
        }

2.3 jsonPase implementation

JSON.parse(JSON.stringify(xxx));

This method is not recommended seriously. There will be the following problems:

1. If there is a time object in obj, the result of JSON.stringify and then JSON.parse will only be in the form of string. Not the time object;

2. If there are RegExp and Error objects in obj, the serialized result will only get empty objects;

3. If there is a function, undefined in obj, the function or undefined will be lost in the result of serialization;

4. If there are NaN, Infinity and - Infinity in obj, the serialized result will become null;

5. JSON.stringify() can only serialize the enumerable properties of an object. For example, if an object in obj is generated by a constructor, the constructor of the object will be discarded after using JSON.parse(JSON.stringify(obj)) deep copy;

2.4 perfect implementation of Es5

To implement deep copy, we must consider all data types of js to process Boolean, Number, String, Function, Array, Date, RegExp, Undefined, Null, Object
And without considering the symbol data type and object property circular reference:

     //Define an employee personal object
        let obj = {
            name:"cc",
            age:30,
            job:{type:"porgrammer",com:"ali"},
            cars:["passat","bmw"],
            working:function(str){
                console.log(`I am ${this.name},I am ${str}`)
            },
            da:new Date(),
            reg: new RegExp(),
            xx:undefined
        }
        
        //This function only gets the employee's vehicle basic information name and vehicle
        let getPerInf = function(obj){
            // var newObj = shshallowClone(obj);
            // let newObj = deepClone(obj);
            let newObj = deepCuClone(obj);
            if(newObj){
                if(newObj.age){
                    delete newObj.age;
                }
                if(newObj.job){
                    // delete newObj.job;
                    newObj.job.com = '360'
                }        
            }
            return newObj;
        }


//Loop references and symbol cases are not considered

function  deepCuClone(data) {
      const type = this.judgeType(data);
      let obj;
      if (type === 'array') {
        obj = [];
      } else if (type === 'object') {
        obj = {};
      } else {
    // No more next level
        return data;
      }
      if (type === 'array') {
        // eslint-disable-next-line
        for (let i = 0, len = data.length; i < len; i++) {
          obj.push(this.deepCuClone(data[i]));
        }
      } else if (type === 'object') {
        // The method on the prototype is also copied
        // eslint-disable-next-line
        for (const key in data) {
          obj[key] = this.deepCuClone(data[key]);
        }
      }
      return obj;
    }

function  judgeType(obj) {
  // tostring will return a constructor corresponding to a different label
      const toString = Object.prototype.toString;
      const map = {
        '[object Boolean]': 'boolean',
        '[object Number]': 'number',
        '[object String]': 'string',
        '[object Function]': 'function',
        '[object Array]': 'array',
        '[object Date]': 'date',
        '[object RegExp]': 'regExp',
        '[object Undefined]': 'undefined',
        '[object Null]': 'null',
        '[object Object]': 'object',
      };
      if (obj instanceof Element) {
        return 'element';
      }
      return map[toString.call(obj)];
    }
    console.log(obj);
    console.log(JSON.parse(JSON.stringify(obj)));
    console.log(getPerInf(obj));
    console.log(obj);
    obj.working("Having dinner");

Result:

3 es6 for deep and shallow copy

3.1 implementation of deconstruction and assignment

    let obj = {
            name:"cc",
            age:30,
            job:{type:"porgrammer",com:"ali"},
            cars:["passat","bmw"],
            working:function(str){
                console.log(`I am ${this.name},I am ${str}`)
            },
            da:new Date(),
            reg: new RegExp(),
            xx:undefined
        };
  let obj0 = {...obj};
    console.log(obj);
    obj0.job.com = "xiaomi";
    console.log(obj0);

Result:


Obviously, the copy implemented by structure assignment is only a shallow copy.

3.2 implementation of Asian method

obj2=Object.assign({},obj);   
    console.log(obj);
    obj2.job.com = "tengxun";
    console.log(obj2);  

Obviously, Asian is still a shallow copy.

The deep copy of es is similar to the method of es5 above.

For deep copy, we should focus on the deep copy of es6 synbol type and circular reference.

4 circular reference and deep copy of symbol type data

Until now, all of the above deep copies cannot be copied when dealing with the problem of object circular reference, and the attribute key added to the object cannot be copied even if it is of symbol type.

Final solution

    // Ultimate solution for circular references and symbol s

/**
 * Determine whether it is a basic data type
 * @param value 
 */
function isPrimitive(value){
  return (typeof value === 'string' || 
  typeof value === 'number' || 
  typeof value === 'symbol' ||
  typeof value === 'boolean')
}

/**
 * Judge whether it is a js object
 * @param value 
 */
function isObject(value){
  return Object.prototype.toString.call(value) === "[object Object]"
}

/**
 * Deep copy a value
 * @param value 
 */
function cloneEnDeep(value){
  // Record the copied value to avoid circular reference
    let memo = {};
    function baseClone(value){
        let res;
        // If it is a basic data type, directly return
        if(isPrimitive(value)){
            return value;
        // If it is a reference data type, we shallowly copy a new value to replace the original value
        }else if(Array.isArray(value)){
            res = [...value];
        }else if(isObject(value)){
            res = {...value};
        }

        // Check whether the property value of the object we copied is a reference data type. If so, recursive copy
        //At the same time, use Reflect to detect properties of Symbol type
        Reflect.ownKeys(res).forEach(key=>{
            if(typeof res[key] === "object" && res[key]!== null){
                //Here we use memo to record the reference address that has been copied. To solve the problem of circular reference
                if(memo[res[key]]){
                res[key] = memo[res[key]];
                }else{
                memo[res[key]] = res[key];
                res[key] = baseClone(res[key])
                }
            }
        })
        return res;  
    }
    return baseClone(value)
}

//======================Testing====================
 //Define an employee personal object
 let objP = {
            name:"cc",
            age:30,
            job:{type:"porgrammer",com:"ali"},
            cars:["passat","bmw"],
            working:function(str){
                console.log(`I am ${this.name},I am ${str}`)
            },
            da:new Date(),
            reg: new RegExp(),
            xx:undefined
        }

objP.job = objP;//Circular reference
console.log(obj);
console.log(getPerInf(obj));
console.log(obj);
obj.working("Having dinner");

Result:

summary

Through the detailed explanation of the above deep and shallow copy, our focus is not to get a perfect deep and shallow copy method, but to improve our deep understanding of the data structure of js language through analysis, so that we can easily read the source code of Niubi library and reduce our own coding bug s.

After testing, the above method meets all types of copies, but it is still recommended to use a third-party library to copy objects in the project:

  • cloneDeep of lodash
  • The extends method of jq
Published 67 original articles, won praise 6, visited 1047
Private letter follow

Tags: JSON Attribute

Posted on Wed, 15 Jan 2020 03:29:08 -0500 by borabora12