Deep copy for the most complete solution

Let's talk about deep copy and shallow copy first

  • Shallow copy

The so-called shallow copy is to copy only the outermost layer, and the references inside are still the same

// Shallow copy
const a = { name: 'xiaoming', age: 23 }
const b = {}
for (let key in a){
  b[key] = a[key]
}

console.log(b) // { name: 'xiaoming', age: 23 }
console.log(b === a) // false
console.log(b.name === a.name) // true

  • Deep copy

Deep copy is to copy an object to another new variable, which points to a new heap memory address

// Deep copy

function cloneDeep(target) {
    // ... dosomething for deep copy
}

const a = { name: 'xiaoming', age: 23 }
const b = cloneDeep(a)

console.log(b) // { name: 'xiaoming', age: 23}
console.log(b === a) // false
console.log(b.name === a.name) // false

  • Deep copy implementation code

Common version

function cloneDeep(target) {
    return JSON.parse(JSON.stringify(target))
}

const a = { name: 'xiaoming', age: 23 }
const b = cloneDeep(a)

console.log(b) // { name: 'xiaoming', age: 23 }
console.log(b === a) // false

Although it is OK to use this method most of the time, there are still many problems in this way

    1. If the field value in the object is undefined, the field will disappear directly after conversion
    2. If an object has a field value of RegExp, the field value will become {} after conversion
    3. If the field value of the object is NaN, + - Infinity, the field value becomes null after conversion
    4. If the object has a ring reference, the conversion will directly report an error

    Upgrade

function cloneDeep(target) {
    const temp = {}
    for (const key in target) {
        temp[key] = target[key]
    }
    return temp
}

const a = { name: 'xiaoming', age: 23 }
const b = cloneDeep(a)

console.log(b) // { name: 'xiaoming', age: 23 }
console.log(b === a) // false

  It looks good, but it's still not possible to optimize and recurse in case of deep layers.

function cloneDeep(target) {
    // Direct return of basic data type
    if (typeof target !== 'object') {
        return target
    }

    // Special handling of reference data types
    const temp = {}
    for (const key in target) {
        // recursion
        temp[key] = cloneDeep(target[key])
    }
    return temp
}

const a = {
    name: 'xiaoming',
    age: 23,
    hobbies: { sports: 'swim', game: 'lol' }
}
const b = cloneDeep(a)

console.log(b === a) // false

  But now you can only copy objects, not arrays. We're optimizing.

function cloneDeep(target) {
    // Direct return of basic data type
    if (typeof target !== 'object') {
        return target
    }

    // Special handling of reference data types
    // Determine array or object
    const temp = Array.isArray(target) ? [] : {}
    for (const key in target) {
        // recursion
        temp[key] = cloneDeep(target[key])
    }
    return temp
}

const a = {
    name: 'xiaoming',
    age: 23,
    hobbies: { sports: 'swim', game: 'lol' },
    works: ['2020', '2021']
}
const b = cloneDeep(a)

console.log(b === a) // false

  But it does not solve an important problem, the problem of ring reference.

  • What is a circular reference

When an attribute in object 1 points to object 2 and an attribute in object 2 points to object 1, a circular reference will appear (of course, there is more than one case, but the principle is the same). Let's explain it through code.

  let obj1 = {};
  let obj2 = {
         b: obj1
      };
  obj1.a = obj2;

JSON.tostringify cannot serialize an infinitely referenced object into a JOSN string.

The following is an explanation of how JSON.stringify() converts the value to the corresponding JSON format:

  • If the transformation value has a toJSON() method, the method defines what value will be serialized.
  • Properties of non array objects cannot be guaranteed to appear in a serialized string in a specific order.
  • The wrapper objects of Boolean value, number and string will be automatically converted to the corresponding original value during serialization.
  • Undefined, arbitrary functions, and symbol values are ignored during serialization (when they appear in the property values of non array objects) or converted to null (when they appear in arrays). When functions and undefined are converted separately, undefined will be returned, such as JSON.stringify(function(){}) or JSON.stringify(undefined)
  • Executing this method on objects that contain circular references (objects refer to each other to form an infinite loop) will throw an error.
  • All attributes with symbol as the attribute key are completely ignored, even if they are forcibly specified in the replace parameter.
  • Date date calls toJSON() to convert it into a string (the same as date. Toisstring()), so it will be treated as a string.
  • Values in NaN and Infinity formats and null will be treated as null.
  • Other types of objects, including Map/Set/WeakMap/WeakSet, serialize only enumerable properties.

Of course, the solution to the above can also be written in this way

  let obj1 = {
  };
  let obj2 = {
    b: obj1
  };
  obj1.a = obj2;
  let c = JSON.decycle(obj1);
  JSON.stringify(c)​

In combination with the above problems, upgrade the deep copy

function cloneDeep(target, map = new Map()) {
    // Direct return of basic data type
    if (typeof target !== 'object') {
        return target
    }
    // Special handling of reference data types
    // Determine array or object
    const temp = Array.isArray(target) ? [] : {}
    if (map.get(target)) {
        // Return directly if it already exists
        return map.get(target)
    }
   // If it does not exist, it is set for the first time
    map.set(target, temp)

    for (const key in target) {
        // recursion
        temp[key] = cloneDeep(target[key], map)
    }
    return temp
}

const a = {
    name: 'xiaoming',
    age: 23,
    hobbies: { sports: 'swim', game: 'lol' },
    works: ['2020', '2021']
}
a.key = a // Ring reference
const b = cloneDeep(a)

console.log(b === a) // false

It seems OK, but it can't realize all the requirements. It can only realize some common arrays, objects and so on.

But in fact, reference data types are not only arrays and objects. We also have to solve the problem of copying the following reference types. How to judge the respective types of each reference data type? You can use Object.prototype.toString.call()

This will return all data types as follows;

type
MapObject.prototype.toString.call(new Map())[object Map]
SetObject.prototype.toString.call(new Set())[object Set]
ArrayObject.prototype.toString.call([])[object Array]
ObjectObject.prototype.toString.call({})[object Object]
SymbolObject.prototype.toString.call(Symbol())[object Symbol]
RegExpObject.prototype.toString.call(new RegExp())[object RegExp]
FunctionObject.prototype.toString.call(function() {})[object Function]

The tree types are divided into two categories: the first is ergodic and the second is non ergodic.

// Traversable type
const mapTag = '[object Map]';
const setTag = '[object Set]';
const arrayTag = '[object Array]';
const objectTag = '[object Object]';

// Non traversable type
const symbolTag = '[object Symbol]';
const regexpTag = '[object RegExp]';
const funcTag = '[object Function]';

// Store traversable types in an array
const canForArr = ['[object Map]', '[object Set]','[object Array]', '[object Object]']

// There will be an array of non traversable types
const noForArr = ['[object Symbol]', '[object RegExp]', '[object Function]']

// Function for judging type
function checkType(target) {
    return Object.prototype.toString.call(target)
}
// temp to determine the reference type
function checkTemp(target) {
    const c = target.constructor
    return new c()
}

  Summarize the two methods of judging

// Method of copying Function
function cloneFunction(func) {
    const bodyReg = /(?<={)(.|\n)+(?=})/m;
    const paramReg = /(?<=\().+(?=\)\s+{)/;
    const funcString = func.toString();
    if (func.prototype) {
        const param = paramReg.exec(funcString);
        const body = bodyReg.exec(funcString);
        if (body) {
            if (param) {
                const paramArr = param[0].split(',');
                return new Function(...paramArr, body[0]);
            } else {
                return new Function(body[0]);
            }
        } else {
            return null;
        }
    } else {
        return eval(funcString);
    }
}

// Method of copying Symbol
function cloneSymbol(targe) {
    return Object(Symbol.prototype.valueOf.call(targe));
}

// How to copy RegExp
function cloneReg(targe) {
    const reFlags = /\w*$/;
    const result = new targe.constructor(targe.source, reFlags.exec(targe));
    result.lastIndex = targe.lastIndex;
    return result;
}

  Combine the final version

function cloneDeep(target, map = new Map()) {
    // Get type
    const type = checkType(target)
    // Direct return of basic data type
    if (!canForArr.concat(noForArr).includes(type)) return target
    // Judge Function, RegExp, Symbol
    if (type === funcTag) return cloneFunction(target)
    if (type === regexpTag) return cloneReg(target)
    if (type === symbolTag) return cloneSymbol(target)

    // Special handling of reference data types
    const temp = checkTemp(target)

    if (map.get(target)) {
        // Return directly if it already exists
        return map.get(target)
    }
    // If it does not exist, it is set for the first time
    map.set(target, temp)

    // Processing Map types
    if (type === mapTag) {
        target.forEach((value, key) => {
            temp.set(key, cloneDeep(value, map))
        })

        return temp
    }

    // Process Set type
    if (type === setTag) {
        target.forEach(value => {
            temp.add(cloneDeep(value, map))
        })

        return temp
    }

    // Processing data and objects
    for (const key in target) {
        // recursion
        temp[key] = cloneDeep(target[key], map)
    }
    return temp
}


const a = {
    name: 'xiaoming',
    age: 23,
    hobbies: { sports: 'swim', game: 'lol' },
    works: ['2020', '2021']
    map: new Map([['aaa', 0001], ['bbb', 0002]]),
    set: new Set([1, 2, 3]),
    func: (do) => `${do}!`,
    sym: Symbol(1111),
    reg: new RegExp(/oooooooooo/g),
}
a.key = a // Ring reference

const b = cloneDeep(a)
console.log(b)
console.log(b === a) // false

Tags: Javascript

Posted on Wed, 13 Oct 2021 14:34:30 -0400 by stuartbates