JavaScript series: 1. Hand tearing the depth copy in JS

Time:2020-11-16

This article first appeared in nuggets, address JavaScript series: 1. Hand tear JS deep copy

preface

Interviewer: how to write a deep copy?
Applicant: JSON.parse ( JSON.stringify (object))
Interviewer: what are the advantages and disadvantages?
Applicant: cannot serialize function, ignore undefined
Interviewer: is there any other way? How to solve circular references?
Candidate: for loop…

1、 Data type

1. Basic data type

Number、String、Boolean、Null、undefined、Symbol、Bigint

Bigint is a newly introduced basic data type

2. Reference data type

Object, array, function, etc

Data type is not the focus of this article, the focus is to achieve deep and shallow copy

The following is the object to be copied. The following code will directly use $obj and will not be declared again
// lmran
var $obj = {
    func: function () {
        console.log('this is function')
    },
    date: new Date(),    
    symbol: Symbol(),
    a: null,
    b: undefined,
    c: {
        a: 1
    },
    e: new RegExp('regexp'),
    f: new Error('error')
}

$obj.c.d = $obj

2、 Light copy

1. What is shallow copy

In a word, it can be said that: for an object, if its first level attribute value is a basic data type, it will completely copy a copy of data; if it is a reference type, it will copy the memory address. It’s really a light copy [chuckles]

2. Implementation

Object.assign()

// lmran
let obj1 = {
    name: 'yang',
    res: {
        value: 123
    }
}

let obj2 = Object.assign({}, obj1)
obj2.res.value = 456
console.log(obj2) // {name: "haha", res: {value: 456}}
console.log(obj1) // {name: "haha", res: {value: 456}}
obj2.name = 'haha'
console.log(obj2) // {name: "haha", res: {value: 456}}
console.log(obj1) // {name: "yang", res: {value: 456}}

Unfolding grammarSpread

// lmran
let obj1 = {
    name: 'yang',
    res: {
        value: 123
    }
}

let {...obj2} = obj1
obj2.res.value = 456
console.log(obj2) // {name: "haha", res: {value: 456}}
console.log(obj1) // {name: "haha", res: {value: 456}}
obj2.name = 'haha'
console.log(obj2) // {name: "haha", res: {value: 456}}
console.log(obj1) // {name: "yang", res: {value: 456}}

Array.prototype.slice

 // lmran
 const arr1 = [
     'yang',
     {
         value: 123
     }
 ];
 
 const arr2 = arr1.slice(0);
 arr2[1].value = 456;
 console.log(arr2); // ["yang", {value: 456}]
 console.log(arr1); // ["yang", {value: 456}]
 arr2[0] = 'haha';
 console.log(arr2); // ["haha", {value: 456}]
 console.log(arr1); // ["yang", {value: 456}]

Array.prototype.concat

  // lmran
  const arr1 = [
      'yang',
      {
          value: 123
      }
  ];
  
  const arr2 = [].concat(arr1);
  arr2[1].value = 456;
  console.log(arr2); // ["yang", {value: 456}]
  console.log(arr1); // ["yang", {value: 456}]
  arr2[0] = 'haha';
  console.log(arr2); // ["haha", {value: 456}]
  console.log(arr1); // ["yang", {value: 456}]
In fact, as long as the original array is not modified and a new array is returned, shallow copy can be realized, such as map, filter, reduce and so on

3、 Deep copy

1. What is deep copy

Deep copy means that both the basic data type and the reference data type are copied again, and there is no common data phenomenon

2. Implementation

Violent version JSON.parse(JSON.stringify(object))

// lmran
let obj = JSON.parse(JSON.stringify($obj))
console.log (obj) // unable to resolve circular reference
/*
    VM348:1 Uncaught TypeError: Converting circular structure to JSON
    at JSON.stringify (<anonymous>)
    at <anonymous>:1:17
*/
delete $obj.c.d
let obj = JSON.parse(JSON.stringify($obj))
console.log (obj) // most properties are missing
/*
    {
        a: null
        c: {a: 1}
        date: "2020-04-05T09:51:32.610Z"
        e: {}
        f: {}
  }
*/

Existing problems:

1. Will be ignoredundefined

2. Will be ignoredsymbol

3. Cannot serialize function

4. Cannot resolve circular referenced objects

5. Not handled correctlynew Date()

6. Cannot handle regular

7. Cannot process new error ()

First edition Basic Edition

Recursively traverse object properties

// lmran
function deepCopy (obj) {
    if (obj === null || typeof obj !== 'object') {
        return obj
    }
    
    let copy = Array.isArray(obj) ? [] : {}
    Object.keys(obj).forEach(v => {
        copy[key] = deepCopy(obj[key])
    })

    return copy
}

deepCopy($obj)
/*
VM601:23 Uncaught RangeError: Maximum call stack size exceeded
    at <anonymous>:23:30
    at Array.forEach (<anonymous>)
    at deepCopy (<anonymous>:23:22)
*/
delete $obj.c.d
deepCopy($obj)
/*
{
    a: null
    b: undefined
    c: {a: 1}
    date: {}
    e: {}
    f: {}
    func: ƒ ()
    symbol: Symbol()
}
*/

The problems are as follows:

1. Cannot resolve circular referenced objects

2. Not handled correctlynew Date()

3. Cannot handle regular

4. Cannot process new error ()

Version 2 addresses circular references

First, solve the problem of loop traversal. The solution is to store the object and object properties in the array to check whether there are any objects that have been traversed in the next traversal. If there are any objects, they will be returned directly. Otherwise, continue to traverse

// lmran
function deepCopy (obj, cache = []) {
    if (obj === null || typeof obj !== 'object') {
        return obj
    }

    const item = cache.filter(item => item.original === obj)[0]
    if (item) return item.copy
    
    let copy = Array.isArray(obj) ? [] : {}
    cache.push({
        original: obj,
        copy
    })

    Object.keys(obj).forEach(key => {
        copy[key] = deepCopy(obj[key], cache)
    })

    return copy
}
deepCopy($obj)
/*
{
    a: null
    b: undefined
    c: {a: 1, d: {…}}
    date: {}
    e: {}
    f: {}
    func: ƒ ()
    symbol: Symbol()
}

Perfect solution to the circular reference problem, but there are still several small problems, all belong to the same class of problems

Third edition addresses special values

For the final processing of several objects, you can judge the type, and just return a new one

// lmran
function deepCopy (obj, cache = []) {
    if (obj === null || typeof obj !== 'object') {
        return obj
    }

    if (Object.prototype.toString.call(obj) === '[object Date]') return new Date(obj)
    if (Object.prototype.toString.call(obj) === '[object RegExp]') return new RegExp(obj)
    if (Object.prototype.toString.call(obj) === '[object Error]') return new Error(obj)
    const item = cache.filter(item => item.original === obj)[0]
    if (item) return item.copy
    
    let copy = Array.isArray(obj) ? [] : {}
    cache.push({
        original: obj,
        copy
    })

    Object.keys(obj).forEach(key => {
        copy[key] = deepCopy(obj[key], cache)
    })

    return copy
}
deepCopy($obj)
/*
{
    a: null
    b: undefined
    c: {a: 1, d: {…}}
    Date: fri APR 10 2020 20:06:08 GMT + 0800 (China standard time) {}
    e: /regexp/
    f: Error: Error: error at deepCopy (<anonymous>:8:74) at <anonymous>:19:21 at Array.forEach (<anonymous>) at deepCopy (<anonymous>:18:22) at <anonymous>:24:1
    func: ƒ ()
    symbol: Symbol()
}
*/

The fourth version solves the same function reference

It seems that the basic functions have been implemented here, but there is still a problem, that is, the function refers to the same memory address. For this problem, most of the Internet directly return or return as objects, including lodash

 const isFunc = typeof value == 'function'
 if (isFunc || !cloneableTags[tag]) {
        return object ? value : {}
 }

Then how to end this problem needs to use Eval function, although this function is not recommended, but it can still solve the problem. There are two functions:Ordinary functionandArrow functionThe only way to distinguish the two is to see if there is oneprototype, yesprototypeProperties belong to ordinary functions. If not, they are arrow functions

// lmran
function copyFunction(func) {
    let fnStr = func.toString()
    return func.prototype ? eval(`(${fnStr})`) : eval(fnStr)
}

function deepCopy (obj, cache = []) {
    if (typeof obj === 'function') {
        return copyFunction(obj)
    }
    if (obj === null || typeof obj !== 'object') {
        return obj
    }

    if (Object.prototype.toString.call(obj) === '[object Date]') return new Date(obj)
    if (Object.prototype.toString.call(obj) === '[object RegExp]') return new RegExp(obj)
    if (Object.prototype.toString.call(obj) === '[object Error]') return new Error(obj)
   
    const item = cache.filter(item => item.original === obj)[0]
    if (item) return item.copy
    
    let copy = Array.isArray(obj) ? [] : {}
    cache.push({
        original: obj,
        copy
    })

    Object.keys(obj).forEach(key => {
        copy[key] = deepCopy(obj[key], cache)
    })

    return copy
}
deepCopy($obj).func === $obj.func // false

##Summary

At this point, deep copy has been fully implemented, but there is no end to learning, we can also consider usingProxyEnhance deep copy performance by blockingsetandgetCome true, of courseObject.defineProperty()it’s fine too. If you are interested, you can check this article. The article introduces in detail. Headline Interviewer: do you know how to realize deep copy of high performance version?

The problem of the same function reference raised by @ brota has been fixed

If there are any problems in the article, you are welcome to point it out. Thank you very much!!!

reference resources

  • The use of MDN Eval function
  • How to write a deep copy of an amazing interviewer?
  • Headline Interviewer: do you know how to achieve deep copy of high performance versions?

Recommended Today

Go carbon version 1.2.0 has been released to improve and optimize the multi scenario support for orm

Carbon is a lightweight, semantic and developer friendly golang time processing library, which supports chain call, lunar calendar, Gorm, xorm and other mainstream orm If you think it’s good, please give it to a star github:github.com/golang-module/carbon gitee:gitee.com/go-package/carbon Installation and use //Using GitHub Library go get -u github.com/golang-module/carbon import ( “github.com/golang-module/carbon”) //Using gitee Library go get […]