This article introduces a simple deep copy handwritten version, focus on solving the deep copy of the circular reference and function reference the same problem, will big guy please bypass

First, data types

1. Basic data types

String, Boolean, Null, undefined, Symbol, Bigint

Bigint is a recently introduced basic data type

2. Reference data types

Object, Array, Function, etc

Data types are not the focus of this article

Here is the object to copy. The rest of the code will use $obj directly and will not declare it againCopy the code
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
Copy the code

Shallow copy

1. What is shallow copy

For an object, a full copy of the data is made for the first attribute value of the base data type, and the memory address is copied for the reference type. It does copy very shallow [chuckles]

2, implementation,

Object.assign()

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

A grammarSpread

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

Array.prototype.slice

 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}]
Copy the code

Array.prototype.concat

  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}]
Copy the code
In fact, for arrays, as long as the original array is not modified, return a new array can achieve shallow copy, such as map, filter, reduce, etcCopy the code

Deep copy

1. What is deep copy

A deep copy is a copy of both the basic and reference data types without sharing data

2, implementation,

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

let obj = JSON.parse(JSON.stringify($obj))
console.log(obj) 			// Cannot resolve circular references
/* VM348:1 Uncaught TypeError: Converting circular structure to JSON at JSON.stringify (
      
       ) at 
       
        :1:17 */
       
      
delete $obj.c.d
let obj = JSON.parse(JSON.stringify($obj))
console.log(obj) 			// Most attributes are missing
/ * {a: null c: {a: 1} the date: "the 2020-04-05 T09:51:32. 610 z" e: {} f: {}} * /
Copy the code

Existing problems:

1. Undefined is ignored

2, will ignore symbol

3. Functions cannot be serialized

4. Cannot resolve objects referenced by loop

Error handling new Date()

Cannot handle regex

7, cannot handle new Error()

First edition Base version

Recursively traverses object properties

function deepCopy (obj) {
    if (obj === null || typeofobj ! = ='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 
      
       :23:30 at Array.forEach (
       
        ) at deepCopy (
        
         :23:22) */
        
       
      
delete $obj.c.d
deepCopy($obj)
/ * {a: null b: undefined c: {a: 1} date: e: {} {} f: {} func: ƒ () symbol: symbol ()} * /
Copy the code

The problems are:

1. Cannot resolve objects referenced by loop

New Date() cannot be handled correctly

Cannot handle regex

4, cannot handle new Error()

The second edition addresses circular references

The solution is to store objects and object properties in an array to see if there are any objects already traversed in the next iteration. If there are, the object will be returned directly. Otherwise, the iteration will continue

function deepCopy (obj, cache = []) {
    if (obj === null || typeofobj ! = ='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 ()}Copy the code

This solves the circular reference problem perfectly, but there are still several minor problems, all of which fall into the same category

The third edition addresses special values

For the final several object processing, you can determine the type, a new return can be

function deepCopy (obj, cache = []) {
    if (obj === null || typeofobj ! = ='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: Apr 10 2020 20:06:08 GMT+0800 {} e: /regexp/ f: Error: Error: error at deepCopy (
      
       :8:74) at 
       
        :19:21 at Array.forEach (
        
         ) at deepCopy (
         
          :18:22) ƒ () symbol: symbol ()} */
         
        
       
      
Copy the code
In addition, Promise, Set, Map, WeakSet, WeakMap can also be treated in the same wayCopy the code

The fourth version addresses the same function references

At this point, the basic functionality seems to be achieved, but there is a problem that functions refer to the same memory address, for which most of the web is either directly returned or returned as an object, including LoDash

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

The question of how to end this problem requires the eval function, which is no longer recommended, but it can still solve the problem. There are two types of functions: normal functions and arrow functions. If there is a prototype attribute, it belongs to normal functions; if there is no arrow function, it belongs to normal functions.

Ordinary functions are function declarations and cannot use eval directly. They need to be wrapped in parentheses to form function expressions. Arrow functions are function expressions themselves

// 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 || typeofobj ! = ='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
Copy the code

conclusion

At this point, deep and shallow copying is complete, but there is no end to learning. We can also consider using Proxy enhancement for deep copy performance, by intercepting sets and get, and of course Object.defineProperty(). If you are interested, you can check out this article, which is very detailed. Headline Interviewer: Do you know how to implement a high performance version of deep copy?

disambiguation

In view of the same problem of function reference raised by @brota, this paper has been fixed, but if the function is a built-in function code still has a problem, the solution is provided, if the function is a built-in function directly return. The built-in function itself has a copy, so there is no problem with the same reference address

function copyFunction(func) {
	let fnStr = func.toString()
	if (fnStr === `function ${func.name}() { [native code] }`) {
		return func
	}
	return func.prototype ? eval(` (${fnStr}) `) : eval(fnStr)
}
Copy the code
Has not yet realized the function: closure of the processing, has not found a way to deal with, know you can leave a comment belowCopy the code

If you have any questions in this article, please kindly point them out. Thank you!!

reference

  • Use of the MDN eval function
  • How to write a deep copy that will impress your interviewer?
  • Headline Interviewer: Do you know how to implement a high-performance version of deep copy?