primers

How do I copy an object in JS? The first thing you might do is just use the assignment statement, for example

let a = {a: 1};
let b = a;
Copy the code

Look at the print

console.log(a)  // {a: 1}
console.log(b)  // {a: 1}
Copy the code

It prints {a: 1}, which is nice, but is it really a copy? Let’s try again:

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

This only modifies object A, but also changes object B. What is the cause of this?

The type of js

Originally, in JS, there are two types of concept, respectively is the basic type and reference type, and the basic type and reference type of the most important difference is in the computer storage location is different;

Basic types: Number, String, Boolen, NULL, undefined, Symbol, Bigint Reference types: Object, Array, RegExp, Date, Function

Primitive types are stored in a stack and have the following properties:

  1. A comparison of primitive types is a comparison of their values
  2. When a primitive type value is copied, a new memory space is created and the value is copied to the new memory space

Reference types, on the other hand, are stored in the heap and have the following properties

  1. A comparison of reference types is a comparison of their addresses (that is, whether Pointers point to the same thing)
  2. A reference value is an object stored in heap memory. The variable holds only the address that points to that memory. When a reference value is copied, it actually copies only the address that points to that memory

A copy of a primitive type can be done using an assignment statement, while a copy of a reference type will get a new pointer to the address and not a new object.

Copy of the js object

Js copy, divided into shallow copy and deep copy

Shallow copy:

Creates a new object with an exact copy of the original object property values. If the property is a primitive type, it copies the value of the primitive type, and if the property is a reference type, it copies the memory address, so if one object changes the address, it affects the other object.Copy the code

Shallow copy scenarios:

1. Object.assign()

The object.assign () method is used to copy the values of all enumerable properties from one or more source objects to target objects. It will return the target object. Note that object.assign () is not a deep copy. If anything, it simply copies the first basic type of the Object. The reference type is also a pointer.

let obj1 = { a: 1, b: {c: 2} } let obj2 = Object.assign({}, obj1); console.log(obj2); // { // a: 1, // b: {c: 2} // } obj1.a = 3; obj1.b.c = 4; console.log(obj1); // { // a: 3, // b: { c: 4} // } console.log(b); // {// a: 1, // b: {c: 4} //} the basic properties of obj2 remain unchanged after the above code changes the object obj1. But when you change object B in object obj1, the corresponding position of object obj2 also changes.Copy the code
2. Expand operator…
let obj1 = { a: 1, b: {c: 2} } let obj2 = {... obj1} console.log(obj2); // {a: 1, b: {c: 2}} obj1.a = 3; obj1.b.c = 4; console.log(obj1); // {a: 3, b: {c: 4}} console.log(obj2); // {a: 1, b: {c: 4}}Copy the code

As you can see from the code above, the expansion operator… Assign is the same as object. assign.

3. Array. Prototype. Slice (), Array. The prototype. The concat ()

Let arr1 = [1, 2, 3, 4]]. let arr2 = arr1.slice(1); console.log(arr2); // [2,[3,4]] arr1[1] = 5; arr1[2][0] = 6; console.log(arr1); / / [1, 5, 6, 4]] the console. The log (arr2); // [2,[6,4]] let arr1 = [1,[2,3]]; Let arr2 = (4 and 6); let arr3 = arr1.concat(arr2); console.log(arr3); // [1,[2,3],4,5,6] arr1[0] = 7; arr1[1][0] = 8; console.log(arr1); / / [7, [8]]. The console log (arr3); / / [1, 8, 3, 4 and 6]Copy the code

Array’s slice and concat methods are not deep copies.

Deep copy:

Deep copy copies all attributes and dynamically allocated memory to which the attributes point. Deep copy occurs when an object is copied along with the object it references. Deep copy is slower and more expensive than shallow copy. The two objects do not affect each other.Copy the code

As mentioned above, since reference objects store Pointers and primitive types store values, we can change the reference type to a primitive type and then reassign the value after converting the primitive type to a reference type, thus obtaining the effect of deep-copy reference types

let a = {a: 1};
let b = JSON.stringify(a);
let c = JSON.parse(b);

console.log(a); // {a: 1}
console.log(b); // {a: 1}

a.b = 2;

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

So we have a new object. Everything looks perfect, but when the object becomes more complex, a new problem emerges

let a = { a: "1", b: undefined, c: Symbol("dd"), fn: function() { return true; }}; console.log(JSON.stringify(a)); // {a: 1}Copy the code

Emmm: a has 3 values, but why only 1 after json.stringify?

Json.stringify turns out to have the following features:

Undefined, symbol, and function are ignored
let obj = { name: 'muyiy', a: undefined, b: Symbol('muyiy'), c: function() {} } console.log(obj); ƒ ()} let b = json.parse (json.stringify (obj)); console.log(b); // {name: "muyiy"}Copy the code
In circular reference cases, an error is reported
let obj = {
    a: 1,
    b: {
        c: 2,
        d: 3
    }
}
obj.a = obj.b;
obj.b.c = obj.a;

let b = JSON.parse(JSON.stringify(obj));  // Uncaught TypeError: Converting circular structure to JSON
Copy the code

In the case of new Date, the conversion result is incorrect

new Date(); // Mon Dec 24 2018 10:59:14 GMT+0800 (China Standard Time) JSON.stringify(new Date()); / / ", "the 2018-12-24 T02:59:25. 776 z" "JSON. Parse (JSON. Stringify (new Date ())); / / "the 2018-12-24 T02:59:41. 523 z"Copy the code

Cannot handle regex

let obj = {
    name: "muyiy",
    a: /'123'/
}
console.log(obj);
// {name: "muyiy", a: /'123'/}

let b = JSON.parse(JSON.stringify(obj));
console.log(b);
// {name: "muyiy", a: {}}
Copy the code

So how can we avoid this?

In fact, to achieve a deep copy of an object, he can be divided into two parts, that is, shallow copy + recursion, you can determine whether the current attribute is an object, if it is an object then recursive operation.

function deepClone1(obj) {
    var target = {};
    for(var key in obj) {
        if( typeof obj[key] === 'object') {
            target[key] = deepClone1(obj[key]);
        } else {
            target[key] = obj[key];
        }
    }
    return target;
}

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

obj1.a = 3;
obj1.b.c = 4;

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

This is a simple implementation of deep copy, but with complex, multi-type objects, the above approach has many drawbacks.

1. Null is not considered

In js design, the first three bits of object are marked with 000, and null is all 0 in the 32-bit representation. Therefore, Typeof NULL also prints object

function deepClone2(obj) { if (obj === null) return null; Var target = {}; for(var key in obj) { if( typeof obj[key] === 'object') { target[key] = deepClone2(obj[key]); } else { target[key] = obj[key]; } } return target; }Copy the code

2. Array compatibility is not considered

In JS, the Typeof array is also an object, which needs to be handled for the array

function deepClone3(obj) { if (obj === null) return null; var target = Array.isArray(obj) ? [] : {}; For (key in obj) {if(typeof obj[key] === 'object') {target[key] = deepClone3(obj[key]); } else { target[key] = obj[key]; } } return target; }Copy the code

3. Not considering circular references in objects

In fact, the idea to solve the circular reference is to judge whether the current value already exists before assigning the value to avoid the circular reference. Here, we can use the WeakMap of ES6 to generate a hash table

function deepClone4(obj, hash = new WeakMap()) { if (obj === null) return null; if (hash.has(obj)) return hash.get(obj); Var target = array.isarray (obj)? [] : {}; hash.set(obj, target); For (var key in obj) {if(typeof obj[key] === 'object') {target[key] = deepClone4(obj[key], hash); } else {target[key] = obj[key]; } } return target; } var a = {b: 1}; a.c = a; console.log(a); // {b:1, c: {b: 1, c:{...... }}} var b = deepClone4(a); console.log(b); // {b:1, c: {b: 1, c:{...... }}}Copy the code

In ES5, you can do the same with arrays.

4. The Symbol is not considered

Whether the Object has a Symbol, you need to use to approach the Object. The getOwnPropertySymbols () or Reflect. OwnKeys (), the following, We use the Object. GetOwnPropertySymbols copies of () to realize the Symbol function deepClone5 (obj, hash = new WeakMap()) { if (obj === null) return null; if (hash.has(obj)) return hash.get(obj); var target = Array.isArray(obj) ? [] : {}; hash.set(obj, target); / / = = = = = = = = = = = = = new code let symKeys = Object. GetOwnPropertySymbols (obj); ForEach (symKey => {if (typeof obj[symKey] === 'object') {target[symKey] = deepClone5(obj[symKey], hash); } else { target[symKey] = obj[symKey]; }}); } // ============= for(var key in obj) { if( typeof obj[key] === 'object') { target[key] = deepClone5(obj[key], hash); } else { target[key] = obj[key]; } } return target; }Copy the code

5. Copy Map and Set files in ES6

Because the typeof Map/Set object for the object, therefore, here we need to use the object. The prototype. ToString, call () method, the following, we need to work out deepClone function transformation

Function deepClone6(obj, hash = new WeakMap()) {if (obj === null) return null; If (hash.has(obj)) return hash.get(obj); / / judgment Symbol let symKeys = Object. GetOwnPropertySymbols (obj); if (symKeys.length) { symKeys.forEach(symKey =>{ if (typeof obj[symKey] === 'object') { target[symKey] = deepClone6(obj[symKey], hash); } else { target[symKey] = obj[symKey]; }}); If (typeof obj === 'object') {let target = null; if (typeof obj === 'object') {let target = null; let result; hash.set(obj, target); let objType = Object.prototype.toString.call(obj); switch (objType) { case '[object Object]': target = {}; break; case '[object Array]': target = []; break; Case '[object Map]': // Process Map object result = new Map(); obj.forEach((value, key) =>{ result.set(key, deepClone6(value, hash)) }) return result break; ForEach ((value) =>{result.add(deepClone6(value, hash))}) return result break; default: break; }} else {return obj; } for (var key in obj) { if (typeof obj[key] === 'object') { target[key] = deepClone6(obj[key], hash); } else { target[key] = obj[key]; } } return target; }Copy the code

Date objects, re’s, and functions

A copy of a Date object can directly return a new Date() object, avoiding reference changes caused by setTime, setYear, etc. The re, and functions are also stored in the heap, but no additional attributes are attached to them

Function deepClone7(obj, hash = new WeakMap()) {if (obj === null) return null; If (hash.has(obj)) return hash.get(obj); / / judgment Symbol let symKeys = Object. GetOwnPropertySymbols (obj); if (symKeys.length) { symKeys.forEach(symKey =>{ if (typeof obj[symKey] === 'object') { target[symKey] = deepClone7(obj[symKey], hash); } else { target[symKey] = obj[symKey]; }}); } / / determine whether object, if not, the direct return, if the object is, continue to implement the if (typeof obj = = = 'object' | | typeof obj = = = 'function') {let target = null; let result; hash.set(obj, target); let objType = Object.prototype.toString.call(obj); switch (objType) { case '[object Object]': target = {}; break; case '[object Array]': target = []; break; Case '[object Map]': // Process Map object result = new Map(); obj.forEach((value, key) =>{ result.set(key, deepClone7(value, hash)) }) return result break; ForEach ((value) =>{result.add(deepClone7(value, hash))}) return result break; Case '[object Date]': return new Date(obj) break; Default: // Directly return the regex, return obj; break; }} else {return obj; } for (var key in obj) { if (typeof obj[key] === 'object') { target[key] = deepClone7(obj[key], hash); } else { target[key] = obj[key]; } } return target; }Copy the code

7. Avoid recursive stack bursts

Since the deep copies above are all using recursion, we all know that recursion in general consumes a lot of memory and has the potential to burst the stack. To solve this problem, we usually have two solutions: tail recursion, and converting recursion into depth or breadth traversal. Finally, the following ideas are briefly provided:

function cloneDeep8(x) { const root = {}; // const loopList = [{parent: root, key: undefined, data: x,}]; While (loolist.length) {// width first const node = loolist.pop (); const parent = node.parent; const key = node.key; const data = node.data; // initialize the assignment target. If the key is undefined, copy it to the parent element, otherwise copy it to the child element. if (typeof key ! == 'undefined') { res = parent[key] = {}; } for(let k in data) {if (data.hasownProperty (k)) {if (typeof data[k] === 'object') {// Next loop looplist.push ({parent:  res, key: k, data: data[k], }); } else { res[k] = data[k]; } } } } return root; }Copy the code

If you are still interested in deep copy or want to research, you can read the lodash deep copy related code, I believe you will have a further understanding of deep copy