This article will explain shallow copy and deep copy from shallow to deep. The knowledge map is as follows:

What’s the difference between a deep copy and a shallow copy?

A:

Both shallow copy and deep copy create a copy of data.

JS is divided into primitive types and reference types. There is no difference between shallow and deep copies of primitive types. The deep and shallow copies we discuss are only for reference types.

  • Both shallow and deep copies copy values and addresses to address the problem of reference type assignments affecting each other.

  • But shallow copies only replicate at one level, the underlying reference types are still shared memory addresses, and the original and copied objects still interact.

  • Deep copy is an infinite level copy. The original object after deep copy does not interact with the copied object.

Many articles on the web mislead people by thinking that reference type assignments are shallow copies, but you can’t go wrong with shallow copies and deep copies in LoDash, which are used by so many projects.

In order to verify the correctness of the above theory, loDash was used for testing. In LoDash, the shallow copy method was Clone and the deep copy method was cloneDeep.

Front knowledge

Comparison of two objects to the same address with the == operator returns true.

Using the == operator to compare two objects to different addresses returns false.

const obj = {}
const newObj = obj

console.log(obj == newObj) // true
Copy the code
const obj = {}
const newObj = {}

console.log(obj == newObj) // false
Copy the code

Reference types assign to each other

Direct assignment, where two objects point to the same address, causes problems with reference types affecting each other:

const obj = {
  name: 'lin'
}

const newObj = obj
obj.name = 'xxx' // Change the original object

console.log('Original object', obj)
console.log('New object', newObj)

console.log('They both point to the same address.', obj == newObj) 
Copy the code

Using shallow copy

Using the Lodash shallow-copy Clone method, we can solve this problem by having both of them point to different addresses:

import { clone } from 'lodash'

const obj = {
  name: 'lin'
}

const newObj = clone(obj)
obj.name = 'xxx'     // Change the original object

console.log('Original object', obj)
console.log('New object', newObj)

console.log('They both point to the same address.', obj == newObj)
Copy the code

However, shallow copy only solves one layer, and further objects will still point to the same address, affecting each other:

import { clone } from 'lodash'

const obj = {
  person: {
    name: 'lin'}}const newObj = clone(obj)
obj.person.name = 'xxx'    // Change the original object

console.log('Original object', obj)
console.log('New object', newObj)

console.log('Deeper to the same address.', obj.person == newObj.person)
Copy the code

Using deep copy

In this case, we need to use deep copy to solve:

import { cloneDeep } from 'lodash'

const obj = {
  person: {
    name: 'lin'}}const newObj = cloneDeep(obj)
obj.person.name = 'xxx' // Change the original object

console.log('Original object', obj)
console.log('New object', newObj)

console.log('Deeper objects point to the same address', obj.person == newObj.person)
Copy the code

With the theory proven, let’s implement shallow copy and deep copy.

Implementing shallow copy

Object.assign

const obj = {
  name: 'lin'
}

const newObj = Object.assign({}, obj)

obj.name = 'xxx' // Change the original object

console.log(newObj) // {name: 'Lin'} The new object remains unchanged

console.log(obj == newObj) // false The two points to different addresses
Copy the code

Slice and concat methods for arrays

const arr = ['lin'.'is'.'handsome']
const newArr = arr.slice(0)

arr[2] = 'rich' // Change the original array

console.log(newArr) // ['lin', 'is', 'handsome']

console.log(arr == newArr) // false The two points to different addresses
Copy the code
const arr = ['lin'.'is'.'handsome']
const newArr = [].concat(arr)

arr[2] = 'rich' // Change the original array

console.log(newArr) // [' Lin ', 'is', 'handsome'] // The new array remains unchanged

console.log(arr == newArr) // false The two points to different addresses
Copy the code

Extended operator

const arr = ['lin'.'is'.'handsome']
const newArr = [...arr]

arr[2] = 'rich' // Change the original array

console.log(newArr) // [' Lin ', 'is', 'handsome'] // The new array remains unchanged

console.log(arr == newArr) // false The two points to different addresses
Copy the code
const obj = {
  name: 'lin'
}

constnewObj = { ... obj } obj.name ='xxx' // Change the original object

console.log(newObj) // {name: 'Lin'} // The new object remains unchanged

console.log(obj == newObj) // false The two points to different addresses
Copy the code

Implementing deep copy

Requirements:

  • Supports object, array, date, re copy.
  • Handle primitive types (primitive types return directly; only reference types have the concept of deep copy).
  • Handle Symbol as the key name.
  • Handling functions (functions return directly, there is no point in copying functions, two objects use functions with the same address in memory, there is no problem).
  • Handle DOM elements (DOM elements are returned directly, there is no point copying DOM elements, they all point to the same page).
  • Open up an additional storage space, WeakMap, to solve the problem of cyclic reference recursive stack explosion (introduce another meaning of WeakMap, with garbage collection mechanism, to prevent memory leakage).

First post the answer:

function deepClone (target, hash = new WeakMap(a)) { // Create an extra storage space, WeakMap, to store the current object
  if (target === null) return target // If null, no copy operation is performed
  if (target instanceof Date) return new Date(target) // Processing date
  if (target instanceof RegExp) return new RegExp(target) // Process the re
  if (target instanceof HTMLElement) return target // Handle DOM elements

  if (typeoftarget ! = ='object') return target // Handle primitive types and functions without deep copy, return directly

  // Deep copy is required for reference type
  if (hash.get(target)) return hash.get(target) // When you want to copy the current object, first go to the storage space to find, if there is a direct return
  const cloneTarget = new target.constructor() // Create a new clone object or clone array
  hash.set(target, cloneTarget) // If there is no storage space, store it in the hash

  Reflect.ownKeys(target).forEach(key= > { // introduce reflect. ownKeys to handle Symbol as the key name
    cloneTarget[key] = deepClone(target[key], hash) // Copy each layer recursively
  })
  return cloneTarget // Returns the cloned object
}
Copy the code

Test it out:

const obj = {
  a: true.b: 100.c: 'str'.d: undefined.e: null.f: Symbol('f'),
  g: {
    g1: {} // Deep objects
  },
  h: []./ / array
  i: new Date(), // Date
  j: /abc/./ / regular
  k: function () {}, / / function
  l: [document.getElementById('foo')] // Introduce the meaning of WeakMap to handle DOM elements that may be cleared
}

obj.obj = obj // Circular reference

const name = Symbol('name')
obj[name] = 'lin'  // Symbol is the key

const newObj = deepClone(obj)

console.log(newObj)
Copy the code

Next, we’ll break down, step by step, how to write this deep copy.

Front knowledge

To make a decent deep copy by hand, use all of the following.

What is the difference between Typeof and Instanceof?

What is the result of typeof NULL? Why is that?

for in

Object.prototype.constructor

WeakMap

Reflect.ownKeys()

Think concept is much, it doesn’t matter, Alin will take you step by step slowly familiar with.

One-line version

First, a line of code version:

JSON.parse(JSON.stringify(obj))
Copy the code
const obj = {
  person: {
    name: 'lin'}}const newObj = JSON.parse(JSON.stringify(obj))
obj.person.name = 'xxx' // Change the original deep object

console.log(newObj) // {person: {name: 'Lin'}} The new deep object remains unchanged
Copy the code

However, there are drawbacks to this method, which ignores undefined, symbol, and function:

const obj = {
  a: undefined.b: Symbol('b'),
  c: function () {}}const newObj = JSON.parse(JSON.stringify(obj))

console.log(newObj) / / {}
Copy the code

And it doesn’t solve the problem of circular references:

const obj = {
  a: 1
}

obj.obj = obj

const newObj = JSON.parse(JSON.stringify(obj)) / / an error
Copy the code

This one-line version is suitable for deep-copying some simple objects in everyday development. Next, we try to write a deep copy step by step, dealing with various boundary issues.

Start with a shallow copy

function clone (obj) {
  const cloneObj = {} // Create a new object
  for (const key in obj) { // Iterate over the object to be cloned
    cloneObj[key] = obj[key] // Add the properties of the object to be cloned to the new object in turn
  }
  return cloneObj
}
Copy the code

After the shallow copy, the original object and the cloned object point to the same address, which affects each other.

Test it out,

const obj = {
  person: {
    name: 'lin'}}const newObj = clone(obj)
obj.person.name = 'xxx'    // Change the original object

console.log('Original object', obj)
console.log('New object', newObj)

console.log('Deeper to the same address.', obj.person == newObj.person)
Copy the code

Simple version

Deep copy is now implemented recursively so that the original and cloned objects do not interact.

function deepClone (target) {
  if (typeoftarget ! = ='object') { // If it is a primitive type, no further copy is required
    return target
  }
  // If it is a reference type, each layer is copied recursively
  const cloneTarget = {} // Define a clone object
  for (const key in target) { // Iterate over the original object
    cloneTarget[key] = deepClone(target[key]) // Copy each layer recursively
  }
  return cloneTarget // Returns the cloned object
}
Copy the code

Test it out:

const obj = {
  person: {
    name: 'lin'}}const newObj = deepClone(obj)
obj.person.name = 'xxx' // Change the original object

console.log('Original object', obj)
console.log('New object', newObj)

console.log('Deeper to the same address.', obj.person == newObj.person)
Copy the code

Handles arrays, dates, re’s, and NULL

The method above implements the simplest version of deep copy, but it doesn’t handle primitive types like NULL, or the more common reference types like arrays, dates, and re.

Test it out,

const obj = {
  a: [].b: new Date(),
  c: /abc/,
  d: null
}

const newObj = deepClone(obj)

console.log('Original object', obj)
console.log('New object', newObj)
Copy the code

Now let’s deal with this:

function deepClone (target) {
  if (target === null) return target / / null
  if (target instanceof Date) return new Date(target) // Processing date
  if (target instanceof RegExp) return new RegExp(target) // Process the re
  
  if (typeoftarget ! = ='object') return target // Handle primitive types
  
  // Handle objects and arrays
  const cloneTarget = new target.constructor() // Create a new clone object or clone array
  for (const key in target) { // Copy each layer recursively
    cloneTarget[key] = deepClone(target[key]) 
  }
  return cloneTarget
}
Copy the code

Test it out:

const obj = {
  a: [1.2.3].b: new Date(),
  c: /abc/,
  d: null
}

const newObj = deepClone(obj)

console.log('Original object', obj)
console.log('New object', newObj)
Copy the code

New instance constructor ()

You might notice a line of code like this, what does it do with arrays?

const cloneTarget = new target.constructor() // Create a new clone object or clone array
Copy the code

The constructor of an instance is essentially a constructor,

class Person {}

const p1 = new Person()

console.log(p1.constructor === Person) // true
console.log([].constructor === Array)  // true
console.log({}.constructor === Object) // true
Copy the code
console.log(new {}.constructor())  / / {}Is equivalent toconsole.log(new Object()) / / {}
Copy the code
console.log(new [].constructor())  / / {}Is equivalent toconsole.log(new Array()) / / []
Copy the code

In our deep copy function, there is no need to determine the type of array when copying, the original object is an object, create a new clone object, the original object is an array, create a new clone array.

Processing Symbol

The above method does not handle Symbol as a key, test it.

const obj = {}
const name = Symbol('name')
obj[name] = 'lin' // Symbol is the key

const newObj = deepClone(obj)

console.log(newObj) / / {}
Copy the code

You can fix this by changing “for in” to reflect.ownkeys

The reflect. ownKeys method returns an array of the target object’s own property keys. The return value is equal to the Object. GetOwnPropertyNames (target). The concat (Object. GetOwnPropertySymbols (target).

Continue to modify the clone function,

function deepClone (target) {
  if (target === null) return target 
  if (target instanceof Date) return new Date(target)
  if (target instanceof RegExp) return new RegExp(target) 

  if (typeoftarget ! = ='object') return target 

  
  const cloneTarget = new target.constructor() 
  
  / / to Reflect. OwnKeys
  Reflect.ownKeys(target).forEach(key= > { 
    cloneTarget[key] = deepClone(target[key]) // Copy each layer recursively
  })
  return cloneTarget
}
Copy the code

Test it out,

const obj = {}
const name = Symbol('name')
obj[name] = 'lin'

const newObj = deepClone(obj)

console.log(newObj)
Copy the code

Handling circular references

The above method does not handle circular references, so test it.

const obj = {
  a: 1
}
obj.obj = obj

const newObj = deepClone(obj)
Copy the code

The reason is that the object has a circular reference situation, recursion into an infinite loop, resulting in stack memory overflow.

To solve the problem of circular reference, you can create an extra storage space to store the mapping between the current object and the copied object.

When you need to copy the current object, first go to the storage space to find if the object has been copied, if yes, directly return, so that the stack does not recurse all the time causing memory overflow.

function deepClone (target, hash = {}) { // Create an extra storage space to store the mapping between the current object and the copied object
  if (target === null) return target
  if (target instanceof Date) return new Date(target)
  if (target instanceof RegExp) return new RegExp(target)

  if (typeoftarget ! = ='object') return target

  if (hash[target]) return hash[target] // When you want to copy the current object, first go to the storage space to find, if there is a direct return
  const cloneTarget = new target.constructor()
  hash[target] = cloneTarget // If there is none in the storage space, store it in the storage space hash

  Reflect.ownKeys(target).forEach(key= > {
    cloneTarget[key] = deepClone(target[key], hash) // Copy each layer recursively
  })
  return cloneTarget
}
Copy the code

Test it out,

const obj = {
  a: 1
}
obj.obj = obj

const newObj = deepClone(obj)

console.log('Original object', obj)
console.log('New object', newObj)
Copy the code

In the above method, we use the storage space created by objects. This storage space can also be used with Map and WeakMap. Here we optimize WeakMap and cooperate with garbage collection mechanism to prevent memory leakage.

WeakMap

WeakMap structure is similar to Map structure and is used to generate a set of key-value pairs. It is the same as Map except for the following two differences

  • WeakMapOnly objects are accepted as key names (nullValues of other types are not accepted as key names.
const map = new WeakMap()
map.set(1.2)        // TypeError: 1 is not an object!
map.set(Symbol(), 2) // TypeError: Invalid value used as weak map key
map.set(null.2)     // TypeError: Invalid value used as weak map key
Copy the code
  • WeakMapIs not counted in the garbage collection mechanism.

WeakMap is designed so that sometimes we want to put some data on an object, but that creates a reference to that object. Take a look at the following example.

const e1 = document.getElementById('foo')
const e2 = document.getElementById('bar')
const arr = [
  [e1, 'foo elements'],
  [e2, 'the bar elements']]Copy the code

In the above code, E1 and e2 are two objects, and we add some text to these two objects through the ARR array. This forms arR’s reference to E1 and E2.

Once these two objects are no longer needed, we must manually remove the reference, otherwise the garbage collection mechanism will not free the memory occupied by E1 and E2.

// when e1 and E2 are not required
// The reference must be removed manually
arr[0] = null
arr[1] = null
Copy the code

So this is obviously an inconvenient way to write it. If you forget to write, you will cause a memory leak.

WeakMap was created to solve this problem, and its key names refer to objects that are weak references, meaning that the garbage collection mechanism does not take that reference into account. Therefore, as long as all other references to the referenced object are cleared, the garbage collection mechanism frees the memory occupied by the object. In other words, once it is no longer needed, the key name object and the corresponding key value pair in WeakMap will disappear automatically, without manually deleting the reference.

Basically, if you want to add data to an object and don’t want to interfere with garbage collection, you can use WeakMap. A typical application scenario is when a WeakMap structure can be used to add data to a DOM element of a web page. When the DOM element is cleared, its corresponding WeakMap record is automatically removed.

const wm = new WeakMap(a)const element = document.getElementById('example')
wm.set(element, 'some information')
wm.get(element) // "some information"
Copy the code

In the above code, create a New WeakMap instance. Then, a DOM node is stored in the instance as the key name, and some additional information is stored in WeakMap as the key value. In this case, a WeakMap reference to an Element is a weak reference and is not counted in the garbage collection mechanism.

That is, the DOM node object above has a reference count of 1, not 2. At this point, once the reference to the node is removed, its memory is freed by the garbage collection mechanism. The key value pair saved by WeakMap will also disappear automatically.

In short, the special occasion of WeakMap is that the object corresponding to its key may disappear in the future. The WeakMap structure helps prevent memory leaks.

The introduction of WeakMap

Understanding the role of WeakMap, we continue to optimize the deep copy function,

Storage space to change the object to WeakMap, WeakMap is mainly to deal with often deleted DOM elements, in the deep copy function is also added to the DOM element processing.

If the copied object is a DOM element, it returns the same object. There is no point in copying DOM elements.

function deepClone (target, hash = new WeakMap(a)) { // Create an extra storage space, WeakMap, to store the current object
  if (target === null) return target
  if (target instanceof Date) return new Date(target)
  if (target instanceof RegExp) return new RegExp(target)
  if (target instanceof HTMLElement) return target // Handle DOM elements

  if (typeoftarget ! = ='object') return target

  if (hash.get(target)) return hash.get(target) // When you want to copy the current object, first go to the storage space to find, if there is a direct return
  const cloneTarget = new target.constructor()
  hash.set(target, cloneTarget) // If there is no storage space, store it in the hash

  Reflect.ownKeys(target).forEach(key= > {
    cloneTarget[key] = deepClone(target[key], hash) // Copy each layer recursively
  })
  return cloneTarget
}
Copy the code

Test it out,

const obj = {
  domArr: [document.getElementById('foo')]}const newObj = deepClone(obj)

console.log(newObj)
Copy the code

At this point, write a deep copy by hand in the interview scenario, almost up to the ceiling, after all, the interview environment of 10 minutes to write a piece of code, can write the above functions, is very impressive.

As for other boundary cases, let’s just talk about them.

More boundary cases

In fact, the above deep-copy method has many defects, many types of objects are not implemented copy, after all, JS standard built-in objects are too many, to consider all the boundary cases, will make the deep-copy function very complicated.

JavaScript standard built-in objects

It took us 14 lines to implement a decent deep copy, but the copy function in LoDash takes more than 14 lines just to define the data type.

/** `Object#toString` result references. */
const argsTag = '[object Arguments]'
const arrayTag = '[object Array]'
const boolTag = '[object Boolean]'
const dateTag = '[object Date]'
const errorTag = '[object Error]'
const mapTag = '[object Map]'
const numberTag = '[object Number]'
const objectTag = '[object Object]'
const regexpTag = '[object RegExp]'
const setTag = '[object Set]'
const stringTag = '[object String]'
const symbolTag = '[object Symbol]'
const weakMapTag = '[object WeakMap]'

const arrayBufferTag = '[object ArrayBuffer]'
const dataViewTag = '[object DataView]'
const float32Tag = '[object Float32Array]'
const float64Tag = '[object Float64Array]'
const int8Tag = '[object Int8Array]'
const int16Tag = '[object Int16Array]'
const int32Tag = '[object Int32Array]'
const uint8Tag = '[object Uint8Array]'
const uint8ClampedTag = '[object Uint8ClampedArray]'
const uint16Tag = '[object Uint16Array]'
const uint32Tag = '[object Uint32Array]'
Copy the code

Tool function methods, packaged more than 20, a deep copy of the function code may add up to nearly a thousand lines.

import Stack from './Stack.js'
import arrayEach from './arrayEach.js'
import assignValue from './assignValue.js'
import cloneBuffer from './cloneBuffer.js'
import copyArray from './copyArray.js'
import copyObject from './copyObject.js'
import cloneArrayBuffer from './cloneArrayBuffer.js'
import cloneDataView from './cloneDataView.js'
import cloneRegExp from './cloneRegExp.js'
import cloneSymbol from './cloneSymbol.js'
import cloneTypedArray from './cloneTypedArray.js'
import copySymbols from './copySymbols.js'
import copySymbolsIn from './copySymbolsIn.js'
import getAllKeys from './getAllKeys.js'
import getAllKeysIn from './getAllKeysIn.js'
import getTag from './getTag.js'
import initCloneObject from './initCloneObject.js'
import isBuffer from '.. /isBuffer.js'
import isObject from '.. /isObject.js'
import isTypedArray from '.. /isTypedArray.js'
import keys from '.. /keys.js'
import keysIn from '.. /keysIn.js'
Copy the code

Lodash doesn’t even use for in or Reflector.ownkeys internally, and has rewritten the traversal method to improve performance.

// arrayEach.js
function arrayEach(array, iteratee) {
  let index = -1
  const length = array.length

  while (++index < length) {
    if (iteratee(array[index], index, array) === false) {
      break}}return array
}
Copy the code

Alin can only understand, but not speak well, so post some of the bigwigs’ articles.

How does Lodash implement deep copy

How to write a deep copy that will impress your interviewer?

In daily development, if you want to use deep copy, in order to be compatible with various boundary cases, generally use three-party library, recommend two:

  • lodash
  • xe-utils

conclusion

As for the choice between shallow copy and deep copy, it is safe to use deep copy for all copies, and generally direct reference to the third party library, after all, write your own deep copy, various boundary cases sometimes do not take into account.

This behavior will only appear in the interview scene 😅, interview scene can write out a deep copy of this article, and can speak the lodash source code implementation ideas, also about.

What’s wrong with a shallow copy if you just copy one layer of objects, as long as you can solve the problem of reference types being assigned to each other?

Also, if json.parse (json.stringify (object)) can do what you want, but you have to introduce LoDash’s cloneDeep method, don’t you just increase the package size of your project? Parse (json.stringify (object)) with json.stringify (object)

Of course, if the team has specifications, in order to unify the code style or to avoid potential risks, all use three-party library method, it is ok to increase the volume of packaging, after all, enterprise projects still need to be more rigorous.

A black cat and a white cat is as good as he can catch mice.

If my article is helpful to you, your 👍 is my biggest support ^_^

I’m Allyn, export insight technology, goodbye!