preface

Deep cloning (deep copy) has always been the beginning, intermediate front-end interview is often asked the topic, the implementation of online introduction has its own advantages, generally can be summarized as three ways:

  1. JSON.stringify+JSON.parseThat’s easy to understand;
  2. Fully determine the type, according to the type of different processing
  3. 2, simplify the type judgment process

The first two are more common and basic, so today we’re going to focus on the third.

Read the full article and you will learn:

  1. More concise deep clone way
  2. Object.getOwnPropertyDescriptors()api
  3. General method of type determination

Problem analysis

Deep copy is, of course, relative to shallow copy. We all know that a reference datatype variable stores a reference to the data, that is, a pointer to the memory space, so if we assign the same way as a simple datatype, we can only copy a reference to the pointer, and we are not really cloning the data.

It’s easy to understand from this example:

const obj1 = {
    name: 'superman'
}
const obj2 = obj1;
obj1.name = 'Front end cutter';
console.log(obj2.name); // cut the front end
Copy the code

Therefore, deep cloning is to solve the problem that reference data types cannot be copied through assignment.

Reference data type

Let’s list the types of reference data:

  • Before ES6: Object, Array, Date, RegExp, Error,
  • After ES6: Map, Set, WeakMap, WeakSet,

Therefore, if we want to clone deeply, we need to traverse the data and adopt the corresponding clone mode according to the type. Of course, recursion is a good choice because data can be nested in multiple layers.

Rough version

function deepClone(obj) {
    let res = {};
    // General method of type determination
    function getType(obj) {
        return Object.prototype.toString.call(obj).replaceAll(new RegExp(/\[|\]|object /g), "");
    }
    const type = getType(obj);
    const reference = ["Set"."WeakSet"."Map"."WeakMap"."RegExp"."Date"."Error"];
    if (type === "Object") {
        for (const key in obj) {
            if (Object.hasOwnProperty.call(obj, key)) { res[key] = deepClone(obj[key]); }}}else if (type === "Array") {
        console.log('array obj', obj);
        obj.forEach((e, i) = > {
            res[i] = deepClone(e);
        });
    }
    else if (type === "Date") {
        res = new Date(obj);
    } else if (type === "RegExp") {
        res = new RegExp(obj);
    } else if (type === "Map") {
        res = new Map(obj);
    } else if (type === "Set") {
        res = new Set(obj);
    } else if (type === "WeakMap") {
        res = new WeakMap(obj);
    } else if (type === "WeakSet") {
        res = new WeakSet(obj);
    }else if (type === "Error") {
        res = new Error(obj);
    }
     else {
        res = obj;
    }
    return res;
}
Copy the code

This is actually the second way we mentioned earlier, which is silly, right? You can see at a glance that there is a lot of redundant code that can be merged.

Let’s start with the most basic optimization:

Merge redundant code

Merge code that is immediately redundant.

function deepClone(obj) {
    let res = null;
    // General method of type determination
    function getType(obj) {
        return Object.prototype.toString.call(obj).replaceAll(new RegExp(/\[|\]|object /g), "");
    }
    const type = getType(obj);
    const reference = ["Set"."WeakSet"."Map"."WeakMap"."RegExp"."Date"."Error"];
    if (type === "Object") {
        res = {};
        for (const key in obj) {
            if (Object.hasOwnProperty.call(obj, key)) { res[key] = deepClone(obj[key]); }}}else if (type === "Array") {
        console.log('array obj', obj);
        res = [];
        obj.forEach((e, i) = > {
            res[i] = deepClone(e);
        });
    }
    // Optimize this part of the redundancy judgment
    // else if (type === "Date") {
    // res = new Date(obj);
    // } else if (type === "RegExp") {
    // res = new RegExp(obj);
    // } else if (type === "Map") {
    // res = new Map(obj);
    // } else if (type === "Set") {
    // res = new Set(obj);
    // } else if (type === "WeakMap") {
    // res = new WeakMap(obj);
    // } else if (type === "WeakSet") {
    // res = new WeakSet(obj);
    // }else if (type === "Error") {
    // res = new Error(obj);
    / /}
    else if (reference.includes(type)) {
        res = new obj.constructor(obj);
    } else {
        res = obj;
    }
    return res;
}
Copy the code

To verify the correctness of the code, let’s use the following data:

const map = new Map(a); map.set("key"."value");
map.set("ConardLi"."coder");

const set = new Set(a); set.add("ConardLi");
set.add("coder");

const target = {
    field1: 1.field2: undefined.field3: {
        child: "child",},field4: [2.4.8].empty: null,
    map,
    set,
    bool: new Boolean(true),
    num: new Number(2),
    str: new String(2),
    symbol: Object(Symbol(1)),
    date: new Date(),
    reg: /\d+/,
    error: new Error(),
    func1: () = > {
        let t = 0;
        console.log("coder", t++);
    },
    func2: function (a, b) {
        returna + b; }};// Test the code
const test1 = deepClone(target);
target.field4.push(9);
console.log('test1: ', test1);
Copy the code

Execution Result:

Is there room for further refinement?

The answer, of course, is yes.

// The method to determine the type is moved outside to avoid multiple executions in the recursive process
const judgeType = origin= > {
    return Object.prototype.toString.call(origin).replaceAll(new RegExp(/\[|\]|object /g), "");
};
const reference = ["Set"."WeakSet"."Map"."WeakMap"."RegExp"."Date"."Error"];
function deepClone(obj) {
    // Define a new object and return it
     // Create objects from obj's prototype
    const cloneObj = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));

    // Iterate over the object and clone the properties
    for (let key of Reflect.ownKeys(obj)) {
        const val = obj[key];
        const type = judgeType(val);
        if (reference.includes(type)) {
            newObj[key] = new val.constructor(val);
        } else if (typeof val === "object"&& val ! = =null) {
            // Recursive clone
            newObj[key] = deepClone(val);
        } else {
            // Basic data types and functionsnewObj[key] = val; }}return newObj;
}
Copy the code

The result is as follows:

  • Object.getOwnPropertyDescriptors() The getDescriptor () method is used to get the descriptors of all of an object’s own properties.
  • Returns a descriptor for all of the specified object’s own properties, or an empty object if there are none.

See MDN for detailed explanation and content

This has the advantage of being able to define in advance the type of data that will eventually be returned.

This implementation refers to the implementation of a big guy on the Internet, I think the cost of understanding a bit high, and the array type of processing is not particularly elegant, return class array.

I modified the code based on my above code, and the modified code is as follows:

function deepClone(obj) {
    let res = null;
    const reference = [Date.RegExp.Set.WeakSet.Map.WeakMap.Error];
    if(reference.includes(obj? .constructor)) { res =new obj.constructor(obj);
    } else if (Array.isArray(obj)) {
        res = [];
        obj.forEach((e, i) = > {
            res[i] = deepClone(e);
        });
    } else if (typeof obj === "Object" && typeofobj ! = =null) {
        res = {};
        for (const key in obj) {
            if (Object.hasOwnProperty.call(obj, key)) { res[key] = deepClone(obj[key]); }}}else {
        res = obj;
    }
    return res;
}
Copy the code

Although there is no advantage in the amount of code, I think the overall understanding cost and your clarity will be better. So what do you think?

Finally, there are circular references to avoid the problem of wireless loops.

We use hash to store loaded objects and return them if they already exist.

function deepClone(obj, hash = new WeakMap(a)) {
    if (hash.has(obj)) {
        return obj;
    }
    let res = null;
    const reference = [Date.RegExp.Set.WeakSet.Map.WeakMap.Error];

    if(reference.includes(obj? .constructor)) { res =new obj.constructor(obj);
    } else if (Array.isArray(obj)) {
        res = [];
        obj.forEach((e, i) = > {
            res[i] = deepClone(e);
        });
    } else if (typeof obj === "Object" && typeofobj ! = =null) {
        res = {};
        for (const key in obj) {
            if (Object.hasOwnProperty.call(obj, key)) { res[key] = deepClone(obj[key]); }}}else {
        res = obj;
    }
    hash.set(obj, res);
    return res;
}
Copy the code

conclusion

There may be many different implementations of deep copy, but the key is to understand the principles and remember the one that is easiest to understand and implement, so that you can be calm when faced with similar problems. Which of the above implementations do you think is better? Welcome to the comments section

More difficult to read, remember to click a “like” to support oh ~ this will be my writing power source ~