leader: deep copy has these five segments. Are you just bronze segments? Still want a raise?

preface

Hello, I'm Lin Sanxin. I was talking about deep copy with the leader a few days ago

  • leader: do you know how to copy an object?
  • Me: I know! Don't you just make a deep copy?
  • leader: How did you copy it?
  • Me: I eat JSON.parse(JSON.stringfy(obj)) directly all over the world
  • leader: brother, if you have time, go to see the deep clone in lodash and see how it is realized

Ha ha, indeed, deep copy has many application scenarios in daily development, and it is also very important. It is necessary to write a qualified deep copy method. How can we write a qualified deep copy method? Or, how can we write a flawless deep copy method?

Deep copy & & shallow copy

Let's talk about what is a deep copy and what is a shallow copy.

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: 'sunshine_lin', age: 23, arr: [] }
const b = {}
for (let key in a){
    b[key] = a[key]
}

console.log(b) // { name: 'sunshine_lin', age: 23, arr: [] }
console.log(b === a) // false
console.log(b.arr === a.arr) // true

Deep copy

Deep copy means that you copy an object to another new variable, which points to a new heap memory address

// Deep copy

function deepClone(target) {
    // ... enable deep copy
}

const a = { name: 'sunshine_lin', age: 23, arr: [] }
const b = deepClone(a)

console.log(b) // { name: 'sunshine_lin', age: 23, arr: [] }
console.log(b === a) // false
console.log(b.arr === a.arr) // false

Gold version

I believe that most people usually implement deep copy in this way

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

const a = { name: 'sunshine_lin', age: 23 }
const b = deepClone(a)

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

Although it is OK to use this method most of the time, it still has many disadvantages

  • 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

Platinum version

Since it is a deep copy of the object, I can create an empty object and copy the values of the original object to be copied one by one!!!

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

const a = { name: 'sunshine_lin', age: 23 }
const b = deepClone(a)

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

But in fact, the above method is not perfect, because we don't know how many layers of objects we want to copy.. As soon as you hear "I don't know how many layers there are", you must think of recursion. Yes, just use recursion.

function deepClone(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] = deepClone(target[key])
    }
    return temp
}

const a = {
    name: 'sunshine_lin',
    age: 23,
    hobbies: { sports: 'Basketball',tv: 'Yongzheng Dynasty ' }
}
const b = deepClone(a)

console.log(b)
// {
//     name: 'sunshine_lin',
//     age: 23,
//     Hobbies: {Sports: 'basketball', tv: 'Yongzheng Dynasty'}
// }
console.log(b === a) // false

Diamond version

Previously, we only considered the object, but did not consider the array, so we need to add the array condition

function deepClone(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] = deepClone(target[key])
    }
    return temp
}

const a = {
    name: 'sunshine_lin',
    age: 23,
    hobbies: { sports: 'Basketball', tv: 'Yongzheng Dynasty ' },
    works: ['2020', '2021']
}
const b = deepClone(a)

console.log(b)
// {
//     name: 'sunshine_lin',
//     age: 23,
//     Hobbies: {Sports: 'basketball', tv: 'Yongzheng Dynasty'},
//     works: ['2020', '2021']
// }
console.log(b === a) // false

Xingyao version

None of the previously implemented methods solve the problem of ring reference

  • JSON.parse(JSON.stringify(target)) reported an error TypeError: Converting circular structure to JSON, which means that the ring reference cannot be processed
  • The recursive method reports an error Maximum call stack size exceeded, which means that the stack will explode if the recursion is not completed

// Ring reference
const a = {}
a.key = a

How to solve the ring reference? In fact, it's not difficult. You need to use the data structure Map of ES6

  • Each time a referenced data type is traversed, it is put into the Map as a key, and the corresponding value is the newly created object temp
  • Every time a reference data type is traversed, go to the Map to find out if there is a corresponding key. If there is, it means that the object has been registered before. Now it encounters the second time, it must be a ring reference. Get value directly according to the key and return value

function deepClone(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] = deepClone(target[key], map)
    }
    return temp
}

const a = {
    name: 'sunshine_lin',
    age: 23,
    hobbies: { sports: 'Basketball', tv: 'Yongzheng Dynasty ' },
    works: ['2020', '2021']
}
a.key = a // Ring reference
const b = deepClone(a)

console.log(b)
// {
//     name: 'sunshine_lin',
//     age: 23,
//     Hobbies: {Sports: 'basketball', tv: 'Yongzheng Dynasty'},
//     works: [ '2020', '2021' ],
//     key: [Circular]
// }
console.log(b === a) // false

King version

We just realized

  • Copy of basic data type
  • Reference array in data type, object

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()

typetoStringresult
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]

Let's first divide the above reference type data into two categories

  • Traversable data type
  • Non traversable data type
// 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()
}

Traversable reference type

It mainly deals with the following four types

  • Map
  • Set
  • Object
  • Array

    function deepClone(target, map = new Map()) {
    
  • const type = checkType(target)

    //Direct return of basic data type

  • if (!canForArr.concat(noForArr).includes(type)) {
  • return 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, deepClone(value, map))
  • })
    +
  • return temp
  • }

    //Process Set type

  • if (type === setTag) {
  • target.forEach(value => {
  • temp.add(deepClone(value, map))
  • })
    +
  • return temp
  • }

    //Processing data and objects
    for (const key in target) {

      // recursion
      temp[key] = deepClone(target[key], map)

    }
    return temp
    }

    const a = {
    name: 'sunshine_lin',
    age: 23,
    Hobbies: {Sports: 'basketball', tv: 'Yongzheng Dynasty'},
    works: ['2020', '2021'],
    map: new Map([['haha', 111], ['xixi', 222]]),
    set: new Set([1, 2, 3]),
    }
    a.key = a / / ring reference
    const b = deepClone(a)

    console.log(b)
    // {
    // name: 'sunshine_lin',
    // age: 23,
    //Hobbies: {Sports: 'basketball', tv: 'Yongzheng Dynasty'},
    // works: [ '2020', '2021' ],
    // map: Map { 'haha' => 111, 'xixi' => 222 },
    // set: Set { 1, 2, 3 },
    // key: [Circular]
    // }
    console.log(b === a) // false

Non traversable reference type

It mainly deals with the following types

  • Symbol
  • RegExp
  • Function

First, write out the methods of copying these three types

// 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;
}

Final version

function deepClone(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, deepClone(value, map))
        })

        return temp
    }

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

        return temp
    }

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


const a = {
    name: 'sunshine_lin',
    age: 23,
    hobbies: { sports: 'Basketball', tv: 'Yongzheng Dynasty ' },
    works: ['2020', '2021'],
    map: new Map([['haha', 111], ['xixi', 222]]),
    set: new Set([1, 2, 3]),
    func: (name, age) => `${name}this year ${age}Years old!!!`,
    sym: Symbol(123),
    reg: new RegExp(/haha/g),
}
a.key = a // Ring reference

const b = deepClone(a)
console.log(b)
// {
//     name: 'sunshine_lin',
//     age: 23,
//     Hobbies: {Sports: 'basketball', tv: 'Yongzheng Dynasty'},
//     works: [ '2020', '2021' ],
//     map: Map { 'haha' => 111, 'xixi' => 222 },
//     set: Set { 1, 2, 3 },
//     func: [Function],
//     sym: [Symbol: Symbol(123)],
//     reg: /haha/g,
//     key: [Circular]
// }
console.log(b === a) // false

epilogue

If you think this article is of little help to you, give a praise and encourage Lin Sanxin, ha ha. Or you can join my fishing group
If you want to join the learning group and fish, please click here [fish](
https://juejin.cn/pin/6969565... ), I will broadcast the mock interview regularly to answer questions and dispel doubts

Tags: Javascript Front-end ECMAScript Interview

Posted on Wed, 24 Nov 2021 01:26:27 -0500 by gotserv