[This is the 10th day of my participation in the Gengwen Challenge. For details, see: Gengwen Challenge]

There are two types of data, the basic data type and the reference data type, which are often copied in JavaScript programming.

Tasking

  • When to use a deep copy?
  • When to use shallow copy?
  • How do I copy a multi-nested object?
  • What kind of deep copy code does it take to qualify?

Shallow copy

How shallow copies work

A shallow copy is defined as creating a new object that accepts the value of the object you want to copy or reference. If the object property is of a basic data type, the value of the basic type is copied to the new object. But if the property is a reference data type, you are copying an address in memory, and if one of the objects changes the address in memory, it will definitely affect the other object.

Common copy methods

object.assign

Assign is an ES6 method of object. This method can be used for multiple purposes, such as combining JS objects. One of the purposes is to make shallow copies. The first argument to the method is the target object of the copy, followed by the source object of the copy (which can also be multiple sources). Object. Assign (target,… sources)

let target = {version: 'es6+'.code: 'async await'};
let source = {support: true};
let newObj = Object.assign(obj, status);
console.log('newObj:', newObj); // {version: "es6+", code: "async await", support: true}
Copy the code

As you can see from the above code, we did simply implement a shallow copy with object.assign. “newObj” is our new copy of the object,

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

As you can see from the above code, we first copy the source into the target Object via object.assign, and then we try to change the b property in the source Object from 2 to 10. From the print console, we can see that the b attribute in the three targets is changed to 10 in the print result, which proves that object. assign temporarily implements the desired copy effect (shallow copy).

But there are a few caveats to using the object.assign method:
  • It does not copy the inherited properties of the object;
  • It does not copy the non-enumerable properties of the object;
  • You can copy properties of type Symbol.
  • The object. assign loop iterates over the properties of the original Object and copies them to the corresponding properties of the target Object. Check out this code to verify that it can copy an Object of type Symbol.
let obj1 = { a: {b:1 }, sym:Symbol(1)};
The object.defineProperty () method directly defines a new property on an Object, or modifies an existing property on an Object, and returns the Object.
Object.defineProperty(obj1, 'innumerable', {value:'Non-enumerable attribute'.enumerable:false
});
let obj2 = {};
Object.assign(obj2,obj1)
obj1.a.b = 2;
console.log('obj1',obj1); // {a: {b: 1}, sym: Symbol(1), innumerable: "unenumerable attribute "}
console.log('obj2',obj2); // {a: {b: 2}, sym: Symbol(1)} did not copy the attribute innumerable to obj2
Copy the code

You can also copy an object of type Symbol using object.assign. However, if you change the value of object 1.a.b, you will also change the value of object 1.a.b, indicating that there is still a common heap memory problem. That is to say, this method can not further copy, but only complete the function of shallow copy.

Extended operator mode

We can also use the extension operators of JS to complete the shallow copy function while constructing the object.

The syntax for the extension operator is: > 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}}

let arr = [1.2.3];
let newArr = [...arr]; // The same effect as arr.slice()
Copy the code

The d extension operator has the same drawback as object.assign, that is, it implements a shallow copy of the same function, but it is more convenient to use the extension operator for shallow copies if the attributes are values of basic types.

Concat copies an array

The concat method for arrays is also a shallow copy, so when concatenating an array with a reference type, you need to be careful to modify the attributes of the elements in the original array because it affects the array that is concatenated after the copy. Concat, however, can only be used for shallow copies of arrays, with limited use scenarios. The code is shown below.

let arr = [1.2.3];
let newArr = arr.concat();
newArr[1] = 100;
console.log(arr);  // [1, 2, 3]
console.log(newArr); // [1, 100, 3]
Copy the code

Slice copy array

The slice method is also limited because it only works with array types. The slice method returns a new array object. The first two arguments to the method determine the start and end times of the truncation without affecting or changing the original array. The syntax for slice is: arr.slice(begin, end);

let arr = [ 1.2, { val: 4}];let newArr = arr.slice();
newArr[2].val = 1000;
console.log(arr);  // [ 1, 2, { val: 1000 } ]
Copy the code

As you can see from the above code, this is where shallow copying is limited — it can only copy one level of objects. If there is nesting of objects, then the shallow copy will do nothing. So deep copy is designed to solve this problem, it can solve the problem of multiple layers of nested objects, complete implementation of copy.

Manually implement a shallow copy

Tasking & Thinking:

  • Make a basic copy of the base type;
  • Create a new store for reference types and copy a layer of object properties.
const shallowClone = (target) = > {
    if (typeof target === 'object'&& target ! = =null) {
        const cloneTarget = Array.isArray(target) ? [] : {};for (let prop in target) {
          	// The hasOwnProperty() method returns a Boolean value indicating whether the object itself has the specified property
            if(target.hasOwnProperty(prop)) { cloneTarget[prop] = target[prop]; }}return cloneTarget;
    } else {
        returntarget; }}Copy the code

As you can see from the above code, you can basically manually implement a shallow copy of the code by using a for loop for an object of a reference type that iterates over the attributes assigned to the target object.

Deep copy

These two objects are independent of each other, not affected by the complete realization of memory separation

How deep copy works

A complete copy of an object from memory to the target object, and from the heap memory open up a new space to store the new object, and the new object changes will not change the original object, the two to achieve real separation.

Common deep copy methods

JSON.stringfy

Json.stringfy () is the simplest deep copy method in the development process. It serializes an object into a JSON string, converts the contents of the object into a string, and generates a new object from the JSON string using the json.parse () method

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

As you can see from the above code, we can initially implement a deep copy of an object by using json. stringfy. By changing the b property of obj1, we can actually see that obj2 is not affected either.

However, there are a few things to note about deep copy using json. stringfy, which I’ll summarize:

  • If the value of the copied object contains function, undefined, or symbol, the key-value pair will disappear after the string is serialized through json. stringify.
  • Copy the Date reference type to a string;
  • Cannot copy non-enumerable properties;
  • Unable to copy object prototype chain;
  • Copying the RegExp reference type becomes an empty object;
  • If an object contains NaN, Infinity, or -infinity, the result of the JSON serialization becomes null.
  • Object loop (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

From the above code, you can see the execution result as shown in the figure below.Using the json.stringify method to implement deep copy objects, although so far there is a lot of functionality that can not be implemented, but this method is sufficient for daily development needs, and is the easiest and fastest. Json. stringify is not sufficient for the data types of other troublesome properties to implement deep copy, so the following methods are needed.

Hand-written recursive implementation

Tasking & implementation ideas

Simple version of the deep copy implementation idea: ①, traversal key, value

(2) determine whether it is an object. If it is an object, the method is called recursively. The other way is to assign

function deepClone(obj) {
    let cloneObj = {}
    for (let key in obj) {
      	cloneObj[key] = typeof obj[key] === 'object' ? deepClone(obj[key]) : cloneObj[key] = obj[key]
    }
    return cloneObj
}
Copy the code

Although it is possible to implement a deep copy using recursion, there are still some problems that are not fully solved, such as:

  • This deep-copy function does not copy non-enumerable properties and Symbol types; This method only recursively copies values for normal reference types. It does not copy correctly for reference types such as Array, Date, RegExp, Error, Function.
  • The properties of the object are looped, i.e. circular references are not resolved. This basic version of the writing method is relatively simple, can cope with most applications. But if you write only such a flawed deep-copy method during the interview, you probably won’t pass.

Improved recursive implementation

In view of the above problems to be solved, I will first tell you what to do through four related theories.

  • For non-enumerable properties that can traverse an object and the Symbol type, we can use the reflect.ownKeys method.
  • If the parameter is of type Date or RegExp, a new instance is generated and returned.
  • Using Object getOwnPropertyDescriptors method can obtain all attributes of the Object, and the corresponding feature, combined with the Object, by the way of the create method to create a new Object, and introduced into the original Object inherit the prototype chain;
  • Use WeakMap type as the Hash table, because WeakMap is a weak reference type, it can effectively prevent memory leaks (you can pay attention to the key difference between Map and WeakMap, use WeakMap here), it is very helpful to detect circular references, if there is a loop, The reference directly returns the value stored by the WeakMap. Ruan Yifeng. WeakMap MDN & ES6
const isComplexDataType = obj= > (typeof obj === 'object' || typeof obj === 'function') && (obj ! = =null)
const deepClone = function (obj, hash = new WeakMap(a)) {
    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 directly returns a new regular object
    // If circular reference is used, weakMap is used to solve the problem
    if (hash.has(obj)) return hash.get(obj)
  	
  	/ * * Object in getOwnPropertyDescriptors () method is used to get all the own properties of an Object descriptor. * a descriptor for all properties of the specified object. If there are no properties of itself, an empty object is returned. * /
    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)) {
        cloneObj[key] = (isComplexDataType(obj[key]) && typeofobj[key] ! = ='function')? deepClone(obj[key], hash) : obj[key] }return cloneObj
}
// Here is the validation code
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    // Set loop as the property of the loop reference
let cloneObj = deepClone(obj)
cloneObj.arr.push(4)
console.log('obj', obj)
console.log('cloneObj', cloneObj)
Copy the code

CloneObj makes a deep copy of obj, and the arR array in cloneObj is changed without affecting the change in obj.arr, as shown in the figure below.