Writing in the front

There are plenty of blogs about deep copying in various tech forums, some of which are better than me, so why do I keep writing this blog when I saw a great quote in a blog I read earlier

Learning is like a mountain. People climb the mountain along different paths and share the scenery they see. You may not be able to see the scenery others see, feel the mood of others. Only oneself go climbing, can see different scenery, experience is more profound.

The original intention of writing a blog is also as a summary of the knowledge points learned, but also hope to give the point open this article some help, in the front end of the development of the road can be less bumpy more hope

The value of the basic type and the value of the reference type

JavaScript variables contain two types of values

  1. Basic type values Basic type values refer to simple data segments that are stored in the stack
let str = 'a';
let num = 1;
Copy the code

In JavaScript basic data types are String, Number, Undefined, Null, Boolean, in ES6, basic data types and defines a new Symbol, so a total of six

Basic types are accessed by value. When a value of a basic type is copied from one variable to another, the values of the two variables are completely independent. Even if one variable changes, the second variable will not be affected

let str1 = 'a';
let str2 = str1;
str2 = 'b';
console.log(str2); //'b'
console.log(str1); //'a'
Copy the code
  1. A reference type value is an instance of a reference type, which is an Object stored in the heap. A reference type is a data structure. The most common types are Object,Array,Function, Date,RegExp,Error, etc. ES6 also provides two new data structures, Set and MAP

How does JavaScript copy reference types

JavaScript assigns values differently to basic and reference types

let obj1 = {a:1};
let obj2 = obj1;
obj2.a = 2;
console.log(obj1); //{a:2}
console.log(obj2); //{a:2}
Copy the code

In this case, only the A property in OBJ1 is changed, but the A property in both OB1 and OBJ2 is changed

When the copy a reference type variable values, and basic types of same value as the value of the variable will be copied to the new variables, the difference is for the value of a variable, it is a pointer to store objects in the heap memory (JS provisions on the object in the heap memory can’t directly access, must want to access the object in the heap memory address, It then retrieves the value of the object at that address, so the value of the reference type is accessed by reference.

The value of the variable, the pointer, is stored on the stack, and when obj1 copies the value of the variable to obj2, obj1,obj2 is just a pointer on the stack, pointing to the same object in the heap, so when obj1 is used to manipulate objects in the heap,obj2 also changes

For example, Xiaoming (obj1) knows his home address (object {a:1}), and then Xiaoming tells Xiaogang (obj2) his home address (copy variable). Xiaogang then knows his home address, and then xiaogang goes to Xiaoming’s house and takes down the door (modify object). When Xiao Ming goes home, he will find that the door is gone. When Xiao Ming and Xiao Gang go to this address, they will see a home without a door.

Shallow copy

Shallow copy can be defined as

Creates a new object that has an exact copy of the property values of the original object. If the property is of a primitive type, the value of the primitive type is copied, and if the property is of a reference type, the memory address is copied, so if one object changes the address, the other object is affected.

Here are some of the shallow copy methods that JavaScript provides

Object.assign

In ES6, the method that copies objects takes the first argument of the object to be copied, and the remaining arguments are the source objects to be copied (which can be multiple).

Syntax: object.assign (target,… sources)

let target = {};
let source = { a: { b: 2}};Object.assign(target, source);
console.log(target); // { a: { b: 2 } };
Copy the code

First we copy the source into the target Object using object.assign. Then we try to change the b property in the source Object from 2 to 10

let target = {};
let source = { a: { b: 2}};Object.assign(target, source);
console.log(target); // { a: { b: 10 } };
source.a.b = 10;
console.log(source); // { a: { b: 10 } };
console.log(target); // { a: { b: 10 } };
Copy the code

The b attribute in all three targets is changed to 10, indicating that object. assign is a shallow copy

Object.assign only creates a new Object in the root property (the first level of the Object), but copies the same memory address if the value of the property is an Object

Other points to note about object. assign are:

  1. Properties inherited by objects are not copied
  2. Properties that cannot be enumerated
  3. The data property/accessor property of the
  4. You can copy the Symbol type

Object.assign traverses all the properties of the source Object (sources) from left to right, and then uses = to assign to the target (target).

let obj1 = {
    a: {b:1
    },
    sym:Symbol(1)};Object.defineProperty(obj1,'innumerable', {value:'Non-enumerable attribute'.enumerable:false
});
let obj2 = {};
Object.assign(obj2,obj1)
obj1.a.b = 2;
console.log('obj1',obj1); 
console.log('obj2',obj2); 
Copy the code

As a side note: In object. assgin, the target and source parameters are wrapped as a basic wrapper type if they are of a basic data type. For more information, see MDN

Extension operator

Using the extension operator, you can clone or copy properties when you construct literal objects

Let cloneObj = {… obj };

let obj = {a:1.b: {c:1}}
letobj2 = {... obj}; obj.a=2;
console.log(obj); //{a:2,b:{c:1}}
console.log(obj2); //{a:1,b:{c:1}}

obj.b.c = 2;
console.log(obj); //{a:2,b:{c:2}}
console.log(obj2); //{a:1,b:{c:2}}
Copy the code

The extension operator object.assign () has the same drawback that it is impossible to copy an attribute whose value is an Object to two different objects, but it is easier to use the extension operator if the attribute is a value of a primitive type

Array.prototype.slice

The slice() method returns a new array object that is a shallow copy of the original array determined by begin and end (not including end). The original array will not be changed.

Syntax: arr. Slice (begin, end);

Before ES6, there was no residual operator. Array.from could use array.prototype. slice to turn arguments Array into a true Array, which returns a shallow copy of the new Array

Array.prototype.slice.call({0: "aaa".length: 1}) //["aaa"]

let arr = [1.2.3.4]
console.log(arr.slice() === arr); //false
Copy the code

Array.prototype.concat

Concat is also a shallow copy of an array, so concatenating an array with a reference type requires the concatenation of attributes of elements in the original array to be reflected in the concatenated array

Deep copy

Shallow copy only on the root attribute in the heap memory creates a new object, copy the values of basic types, but complex data types, that is, the object is to copy the same address, and deep copy is for complex data types in the heap memory has opened up a memory address to hold copy objects and make a copy of the original object, These two objects are independent of each other, that is, they have two different addresses

A complete copy of an object from memory, from the heap memory to open a new area to store the new object, and the new object changes will not affect the original object

A simple deep copy

let obj1 = {
    a: {
        b: 1
    },
    c: 1
};
let obj2 = {};

obj2.a = {}
obj2.c = obj1.c
obj2.a.b = obj1.a.b;
console.log(obj1); //{a:{b:1},c:1};
console.log(obj2); //{a:{b:1},c:1};
obj1.a.b = 2;
console.log(obj1); //{a:{b:2},c:1};
console.log(obj2); //{a:{b:1},c:1};
Copy the code

In the above code, we create an obj2 object, and since obj1’s property a is a reference type, we create a new object for obj2.a, and copy the value of obj1.a.b to obj2.a.b. Since the number 1 is a value of the primitive type, obj2.a will not be affected by changing the value of obj1.a.b because their references are completely two separate objects, completing a simple deep copy

JSON.stringify

Json.stringify () is the most commonly used deep copy method in front-end development. The principle is to serialize an object into a JSON string, convert the contents of the object into a string and save it on disk, and then deserialize the JSON string into a new object with json.parse ()

let obj1 = {
    a:1.b: [1.2.3]}let str = JSON.stringify(obj1)
let obj2 = JSON.parse(str)
console.log(obj2); / / {a: 1, b: [1, 2, 3]}
obj1.a = 2
obj1.b.push(4);
console.log(obj1); / / {2, a: b: [1, 2, 3, 4]}
console.log(obj2); / / {a: 1, b: [1, 2, 3]}
Copy the code

There are a few caveat to implementing deep copy with json.stringify

  1. If there is a function,undefined,symbol in the value of the copied object, the key-value pair will disappear in the JSON string serialized through json.stringify ()
  2. Cannot copy non-enumerable properties, cannot copy the prototype chain of an object
  3. Copying the Date reference type becomes a string
  4. Copying the RegExp reference type becomes an empty object
  5. An object containing NaN, Infinity, and -infinity will render null
  6. (obj[key] = obj)
function Obj() {
    this.func = function () {
        alert(1)};this.obj = {a:1};
    this.arr = [1.2.3];
    this.und = undefined;
    this.reg = / 123 /;
    this.date = new Date(0);
    this.NaN = NaN
    this.infinity = Infinity
    this.sym = Symbol(1)}let obj1 = new Obj();
Object.defineProperty(obj1,'innumerable', {enumerable:false.value:'innumerable'
})
console.log('obj1',obj1);
let str = JSON.stringify(obj1);
let obj2 = JSON.parse(str);
console.log('obj2',obj2);
Copy the code

The printout results are as follows

Obj1’s constructor is an Obj constructor, whereas obj2’s constructor points to Object. For circular references, this is an error

While deep copying objects through the json.stringify () method does a lot of things you can’t do, for everyday development needs (objects and arrays), using this method is the easiest and quickest

Use third-party libraries to implement deep copies of objects

1.lodash

2.jQuery

The above two third-party libraries are very good encapsulation of deep copy methods, interested students can go to further research

Implement a deep copy function yourself

recursive

Here we simply encapsulate a deepClone function. For in iterates over the value of the parameters passed in, calls deepClone again if the value is a reference type, and passes in the value of the first deepClone parameter as an argument to the second deepClone call, or copies directly if it is not a reference type

let obj1 = {
    a: {b:1}};function deepClone(obj) {
    let cloneObj = {}; // Create an object in the heap memory
    for(let key in obj){ // Iterate over the key of the parameter
       if(typeof obj[key] ==='object'){ 
          cloneObj[key] = deepClone(obj[key]) // If the value is an object, the function is called again
       }else{
           cloneObj[key] = obj[key] // The basic type copies the value directly}}return cloneObj 
}
let obj2 = deepClone(obj1);
obj1.a.b = 2;
console.log(obj2); //{a:{b:1}}
Copy the code

But there are many problems

  • First, the deepClone function does not copy non-enumerable properties and Symbol types
  • Here only for the value of the Object reference type do loop iteration, for the Array, the Date, the RegExp, Error, the Function reference types cannot copy right
  • Objects ring, i.e. circular references (e.g., obj1.a = obj)

I summarize the deep copy method

After reading many blogs about deep copying, I’ve come up with a way to deep copy ECMAScript’s native reference types

const isComplexDataType = obj= > (typeof obj === 'object' || typeof obj === 'function') && (obj ! = =null)

const deepClone = function (obj, hash = new WeakMap()) {

    if (obj.constructor === Date) return new Date(obj);   The date object returns a new date object
    if (obj.constructor === RegExp) return new RegExp(obj);  The regular object returns a new regular object

    Loop = The initial obj will find the first obj in the WeakMap and return the cloneObj that was first put into the WeakMap in advance
    if (hash.has(obj)) return hash.get(obj)

    let allDesc = Object.getOwnPropertyDescriptors(obj);     // Iterate over the properties of all the keys passed in
    let cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc); // Inherit the prototype chain

    hash.set(obj, cloneObj)

    for (let key of Reflect.ownKeys(obj)) {   // reflect.ownkeys (obj) can copy non-enumerable attributes and symbol types
        // Call deepClone recursively if the value is a reference type (not a function)
        cloneObj[key] =
            (isComplexDataType(obj[key]) && typeofobj[key] ! = ='function')? deepClone(obj[key], hash) : obj[key]; }return cloneObj;
};

let obj = {
    num: 0.str: ' '.boolean: true.unf: undefined.nul: null.obj: {
        name: 'I am an object'.id: 1
    },
    arr: [0.1.2].func: function () {
        console.log(I'm a function.)},date: new Date(0),
    reg: new RegExp('/ I am a regular /ig'),Symbol('1')]: 1};Object.defineProperty(obj, 'innumerable', {
    enumerable: false.value: 'Non-enumerable attribute'
});

obj = Object.create(obj, Object.getOwnPropertyDescriptors(obj))

obj.loop = obj

let cloneObj = deepClone(obj);

console.log('obj', obj);
console.log('cloneObj', cloneObj);

for (let key of Object.keys(cloneObj)) {
    if (typeof cloneObj[key] === 'object' || typeof cloneObj[key] === 'function') {
        console.log(`${key}The same? `, cloneObj[key] === obj[key])
    }
}
Copy the code

This function has several points

  1. Using the reflect.ownKeys method, you can iterate through the object’s non-enumerable attributes and Symbol type
  2. When the parameter is Date, a new instance of the RegExp type is generated directly
  3. Use Object. GetOwnPropertyDescriptors get all attributes of the Object corresponding characteristics, combined with the Object. The create to create a new Object inheritance to the prototype chain of the original Object
  4. Using WeakMap type as hash table,WeakMap can effectively prevent memory leak because it is a weak reference, and it is very helpful to detect circular reference. If there is a circular reference, the value stored by WeakMap is directly returned

Here, I use congruence to judge whether the properties of the two objects are equal. According to the printed results, although the values are the same, they are completely independent objects in memory

If you want to deep copy a Function, you might want to use the Function constructor or eval? That’s still to be studied

conclusion

  1. The wrapped deepClone method can copy native reference types in ECMAScript, but it is too wide for objects (such as DOM nodes) to copy accurately. However, it is not necessary to copy many specific reference types in daily development. Using json.stringify is still one of the most convenient ways to deep copy objects (although you need to be aware of the disadvantages of json.stringify).

  2. Implementing a full deep copy is very complicated, and there are many boundary cases to consider. Here, I have only deep copied some of the native constructors. For specific reference types, it is recommended to use a full third-party library

  3. It is helpful to understand the characteristics of reference types in JavaScript and solve the special problems related to deep copy. It is also very helpful to improve the basic JavaScript

Thanks for watching

The resources

Deep copy objects in JS

JavaScript Advanced programming edition 3