The problem with copying is mainly with reference types

Shallow copy is different from deep copy

Let’s start with a quick review of JavaScript basics

JavaScript contains values of two different data types: primitive (primitive) and reference types

The basic types are string, number, Boolean, NULL, undefined, symbol, bigInt

The reference types are Object (Object, Array, Function…).

When assigning a value to a variable, the parser must determine whether the value is of a primitive or reference type

  • Basic data types are accessed by value because you can manipulate the actual values stored in variables
  • The value of a reference type is an object stored in memory, and the stack memory stores the identifier of the variable and the location of the object stored in the heap memory. JavaScript does not allow direct access to locations in memory, that is, you cannot directly manipulate the memory space of an object. So when you manipulate an object, you’re actually referring to the object you manipulate rather than the actual object. When you need to access the value of a reference type (such as an object, array, etc.), you first get the address pointer of the object from the stack, and then get the required data from the corresponding heap memory

2. JavaScript variable storage methods — stack and heap

  • The stack: Memory space is automatically allocated and released by the system, which stores values of basic types and address Pointers of reference types
  • The heap: Dynamically allocates memory of variable size and is not automatically freed. It stores values of reference types

The biggest difference between base types and reference types is really the difference between value passing and address passing

  • Value passing: Base types use value passing
let a = 1;
let b = a;
b++;
console.log(a, b) / / 1. 2
Copy the code
  • Address pass: The reference type is address pass, assigning an address stored in stack memory to the variable received
let a = ['a'.'b'.'c'];
let b = a; 
b.push('d');
console.log(a) // ['a', 'b', 'c', 'd']
console.log(b) // ['a', 'b', 'c', 'd']
Copy the code

Analysis:

  • A is an array and a reference type. Assigning to B is assigning the address of A to B, so both a and B refer to the same address (which refers to the actual value of the reference type in the heap).
  • When B changes this value, since A’s address also points to this value, the value of A also changes, just as A rents a room, gives the address of the room to B, and B finds the room through the address. Then any changes B makes to the room must also be visible to A

So how to solve the problem above, here leads to shallow copy or deep copy. JS basic types do not have shallow copy or deep copy issues, mainly for reference types

Shallow copy: The copy level is shallow. Shallow copy refers to copying only the first-layer key-value pairs of an object. If there are other objects in the object, only the address Pointers of nested objects are copied

  • Disadvantages: When an attribute is a reference value (array or object), this cloning method only assigns the reference value to the new target object, that is, once the reference value of the source object or target object is changed, the other object will also change

Deep copy: The copy level is deeper. Deep copy refers to full copy of objects. Even if nested objects are copied, the copied objects do not affect each other. Modifying the properties of one object does not affect the other. The idea is to recursively copy the properties of the object into the object again

Shallow copy

Slice, concat if array, array elements are basic data types, use some array methods such as slice, concat return a new array to achieve copy (this is equivalent to deep copy)

If the elements of an Array are Object, slice and concat copy the Array of objects to a shallow copy, and the Pointers to each element of the Array will still point to the same storage address

let arr = ['one'.'two'.'three'];
let newArr = arr.concat();
newArr.push('four')

console.log(arr)    // ["one", "two", "three"]
console.log(newArr) // ["one", "two", "three", "four"]

let arr = ['one'.'two'.'three'];
let newArr = arr.slice();
newArr.push('four')

console.log(arr)    // ["one", "two", "three"]
console.log(newArr) // ["one", "two", "three", "four"]

let arr = [{a:1}, 'two'.'three'];
let newArr = arr.concat();
newArr[0].a = 2;

console.log(arr)    // [{a: 2},"two","three"]
console.log(newArr) // [{a: 2},"two","three"]
Copy the code

Object assign() this method copies the enumerable properties of any number of source objects to the target Object and returns the target Object. Object assign() is a shallow copy of the Object

let arr = {
  a: 'one'.b: 'two'.c: 'three'
};

let newArr = Object.assign({}, arr)
newArr.d = 'four'
console.log(arr);    // {a: "one", b: "two", c: "three"}
console.log(newArr); // {a: "one", b: "two", c: "three", d: "four"}

let arr = {
  a: 'one'.b: 'two'.c: {a: 1}};let newArr = Object.assign({}, arr)
newArr.c.a = 3;
console.log(arr);    // {a: "one", b: "two", c: {a: 3}}
console.log(newArr); // {a: "one", b: "two", c: {a: 3}}
Copy the code

Object. Assign principle and its implementation

Shallow copy package

How it works: Iterate over an object, then place the property and its value in a new object and return it

function clone(obj) {
  // Only objects are copied
  if (typeofsrc ! = ='object') return;
  // Determine whether to create an array or object based on the type of obj
  let newObj = Obejct.prototype.toString.call(obj) == '[object Array]' ? [] : {};
  for(let prop in newObj) {
    if(newObj.hasOwnProperty(prop)) { newObj[prop] = obj[src]; }}return newObj;
}
Copy the code

Deep copy

Json.parse (json.stringify (arr)) : Applies to objects as well as arrays

let a = {
  name: "tn".book: {
    title: "JS".price: "45"}}let b = JSON.parse(JSON.stringify(a));
console.log(b);
/ / {
// name: "tn",
// book: {title: "JS", price: "45"}
// } 

a.name = "change";
a.book.price = "55";
console.log(a);
/ / {
// name: "change",
// book: {title: "JS", price: "55"}
// } 

console.log(b);
/ / {
// name: "tn",
// book: {title: "JS", price: "45"}
// } 
Copy the code

Changing a reference property in variable A has no effect on variable B, which is the magic of deep copy

After a deep copy of an array, changing the original array page does not affect the copied array

/ / wooden Yi Yang
let a = [0."1"[2.3]].let b = JSON.parse(JSON.stringify( a.slice(1)));console.log(b); / / [" 1 ", [2, 3]]

a[1] = "99";
a[2] [0] = 4;
console.log(a); // [0, "99", [4, 3]]

console.log(b); / / [" 1 ", [2, 3]]
Copy the code

But this approach has limitations

  • ignoreundefined
  • ignoresymbol
  • Non-serializable function
  • Cannot resolve object referenced by loop
  • Not properly handlednew Date()
  • Cannot handle regex

Undefined, symbol, and functions are ignored

/ / wooden Yi Yang
let obj = {
  name: "tn".a: undefined.b: Symbol("tn"),
  c: function() {}}let b = JSON.parse(JSON.stringify(obj));
console.log(b); // {name: "tn"}
Copy the code

An error is reported in circular reference cases

let obj = {
  a: 1.b: {
    c: 2.d: 3
  }
}
obj.a = obj.b;
obj.b.c = obj.a;

let b = JSON.parse(JSON.stringify(obj)); // Uncaught TypeError: Converting circular structure to JSON
Copy the code

The conversion result in the new Date case is incorrect

new Date(a);// Mon Dec 24 2018 10:59:14 GMT+0800 (China Standard Time)

JSON.stringify(new Date()); / / ", "the 2018-12-24 T02:59:25. 776 z" "

JSON.parse(JSON.stringify(new Date())); / / "the 2018-12-24 T02:59:41. 523 z"
Copy the code

The solution is converted to a string or timestamp

let date = (new Date()).valueOf();
JSON.stringify(date); / / "1625905818735"
JSON.parse(JSON.stringify(date)); / / 1625905818735
Copy the code

Regular case

let obj = {
  name: "tn".a: / / '123'
}
console.log(obj); // {name: "tn", a: /'123'/}
let b = JSON.parse(JSON.stringify(obj));
console.log(b); // {name: "tn", a: {}}
Copy the code

ES6 extension operator […] : applies not only to arrays but also to objects. Only raw values can be deeply copied, and shallow copies can be made if there are reference values

Lodash’s deep-copy function

Deep copy package

Principle: Judge the type of attribute value when copying, if the object recursively call the deep copy function, the deep copy is to copy the content of the original object completely and send it to a new memory space, pointing to a new memory address

function deepClone1(src, target) {
  var target = target || {};
  for (let prop in src) {
    if (src.hasOwnProperty(prop)) {
      if(src[prop] ! = =null && typeof(src[prop]) === 'object') {
        target[prop] = Object.prototype.toString.call(src[prop]) == '[object Array]' ? [] : {};
        deepClone(src[prop], target[prop]);
      } else{ target[prop] = src[prop]; }}}return target;
}

// test
var a = {
  name: "tn".book: {
    title: "JS".price: "45"
  },
  a1: undefined.a2: null.a3: 123
}

var b = deepClone(a);
console.log(b);
/ / {
// a1: undefined,
// a2: null,
// a3: 123,
// book: {
// title: "JS",
// price: "45"
/ /},
// name: "tn"
// }
Copy the code

A circular reference

We know that JSON cannot be used for deep-copy circular references, and this throws an exception

Use hash tables

In fact, cyclic detection, set up an array or hash table to store the copied object, when the detection of the current object already exists in the hash table, fetch the value and return

function deepClone2(src, hash = new WeakMap(a)) {
  var target = Object.prototype.toString.call(src) == '[object Array]' ? [] : {};
  if (hash.has(src)) return hash.get(src); // add code to check hash table
  hash.set(src, target); // Add code, hash table set value
  for (let prop in src) {
    if (src.hasOwnProperty(prop)) {
      if(src[prop] ! = =null && typeof(src[prop]) === 'object') {
        target[prop] = deepClone2(src[prop], hash);
      } else{ target[prop] = src[prop]; }}}return target;
}

var a = {
  name: "tn".book: {
    title: "JS".price: "45"
  },
  a1: undefined.a2: null.a3: 123
};
a.circleRef = a;
var b = deepClone2(a);
console.log(b);
/ / {
// a1: undefined,
// a2: null,
// a3: 123,
// book: {title: "JS", price: "45"},
// circleRef: {name: "tn", book: {... }, a1: undefined, a2: null, a3: 123,... },
// name: "tn"
// }
Copy the code

Use an array

WeakMap in ES6 is used to deal with it above, and array can be used to deal with it in ES5

function deepClone2(src, uniqueList) {
  var target = Object.prototype.toString.call(src) == '[object Array]' ? [] : {};
  if(! uniqueList) uniqueList = [];// Add code to initialize the array
  // Data already exists, return saved data
  var uniqueData = find(uniqueList, src);
  if (uniqueData) {
     return uniqueData.target;
  };
  // Data does not exist, save the source data, and the corresponding reference
  uniqueList.push({
      source: src,
      target: target
  });
  for (let prop in src) {
    if (src.hasOwnProperty(prop)) {
      if(src[prop] ! = =null && typeof(src[prop]) === 'object') {
        target[prop] = deepClone2(src[prop], uniqueList);
      } else{ target[prop] = src[prop]; }}}return target;
}

// Test OK with the above use case
Copy the code

The circular reference case is now perfectly solved, but there is another case where the reference is lost. Let’s look at the following example

var obj1 = {};
var obj2 = {a: obj1, b: obj1};

obj2.a === obj2.b; // true

var obj3 = deepClone1(obj2);
obj3.a === obj3.b; // false
Copy the code

Missing references can be problematic in some cases, such as obj2 above, where the key values A and B of obj2 both refer to the same object obj1. After a deep copy using deepClone1, the reference relationship is lost and becomes two different objects

In fact, deepClone2 above solves this problem by storing the copied objects

var obj3 = deepClone2(obj2);
obj3.a === obj3.b; // true
Copy the code

Copies of the Symbol

Symbol is available under ES6 and requires some method to detect the Symble type

  • Method one: Object. GetOwnPropertySymbols (…).

    This method can look up a given object’s symbolic property when returning a? An array of type symbol. Note that each initialized object does not have its own symbol attribute, so this array may be empty unless you have already set the symbol attribute (from MDN) on the object.

    var obj = {};
    var a = Symbol("a"); // Create a new symbol type
    var b = Symbol.for("b"); // Register from the global symbol? Table sets and gets the symbol
    
    obj[a] = "localSymbol";
    obj[b] = "globalSymbol";
    
    var objectSymbols = Object.getOwnPropertySymbols(obj);
    
    console.log(objectSymbols.length); / / 2
    console.log(objectSymbols)         // [Symbol(a), Symbol(b)]
    console.log(objectSymbols[0])      // Symbol(a)
    Copy the code

    The idea is to find whether there is a Symbol attribute first, if found, first through the processing of Symbol, and then to deal with the normal situation

    function deepClone3(src, hash = new WeakMap(a)) {
      var target = Object.prototype.toString.call(src) == '[object Array]' ? [] : {};
      if (hash.has(src)) return hash.get(src); // add code to check hash table
      hash.set(src, target); // Add code, hash table set value
    
      let symKeys = Object.getOwnPropertySymbols(src); / / to find
      if (symKeys.length) { // The search succeeded
        symKeys.forEach(symKey= > {
          if(src[prop] ! = =null && typeof(src[prop]) === 'object') {
            target[symKey] = deepClone3(src[symKey], hash); 
          } else{ target[symKey] = src[symKey]; }}); }for (let prop in src) {
        if (src.hasOwnProperty(prop)) {
          if(src[prop] ! = =null && typeof(src[prop]) === 'object') {
            target[prop] = deepClone3(src[prop], hash);
          } else{ target[prop] = src[prop]; }}}return target;
    }
    
    var a = {
      name: "tn".book: {
        title: "JS".price: "45"
      },
      a1: undefined.a2: null.a3: 123
    };
    var sym1 = Symbol("a"); // Create a new symbol type
    var sym2 = Symbol.for("b"); // Register from the global symbol? Table sets and gets the symbol
    
    a[sym1] = "localSymbol";
    a[sym2] = "globalSymbol";
    
    var b = deepClone3(a);
    console.log(b);
    
    / / {
    // a1: undefined
    // a2: null
    // a3: 123,
    // book: {title: "JS", price: "45"},
    // circleRef: {name: "tn", book: {... }, a1: undefined, a2: null, a3: 123,... },
    // name: "tn",
    // [Symbol(a)]: "localSymbol",
    // [Symbol(b)]: "globalSymbol"
    // }
    Copy the code
  • Reflect. OwnKeys (…)

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

    Reflect.ownKeys({z: 3.y: 2.x: 1}); // [ "z", "y", "x" ]
    Reflect.ownKeys([]); // ["length"]
    
    var sym = Symbol.for("comet");
    var sym2 = Symbol.for("meteor");
    var obj = {[sym]: 0."str": 0."773": 0."0": 0,
              [sym2]: 0."1": 0."8": 0."second str": 0};
    Reflect.ownKeys(obj); // [ "0", "8", "773", "str", "-1", "second str", Symbol(comet), Symbol(meteor) ]
    // Pay attention to the order
    // Indexes in numeric order, 
    // strings in insertion order, 
    // symbols in insertion order
    Copy the code
    function deepClone3(src, hash = new WeakMap(a)) {
      var target = Object.prototype.toString.call(src) == '[object Array]' ? [] : {};
      if (hash.has(src)) return hash.get(src); 
      hash.set(src, target);
      
      Reflect.ownKeys(src).forEach(key= > { / / change
        if(src[key] ! = =null && typeof(src[key]) === 'object') {
          target[key] = deepClone3(src[key], hash); 
        } else{ target[key] = src[key]; }});return target;
    }
    / / test ok
    Copy the code

    We use reflect.ownkeys () to get all the keys, including Symbol, by iterating the assignment to SRC

Crack a recursive stack burst

All of the above are recursive methods, but there is a problem that can burst the stack, the error message is as follows

// RangeError: Maximum call stack size exceeded
Copy the code

See this article: The Ultimate Quest for Deep Copy (99% of people don’t know)

Library implementation

The above approach can meet the needs of the basic scenario, if there are more complex requirements can be implemented themselves. Some frameworks and libraries have corresponding solutions, such as jquery.extend () and LoDash

Application scenarios

Shallow copy Mixins that use vue when you want to copy a copy of a one-layer Array and Object are a complex form of shallow copy

Deep copy Copies the deep-level object data structure. If you want to modify the value of an array or object but keep the original value unchanged, you can use deep copy to create a new array or object

The resources

Deep copy and shallow copy in javascript? How does JavaScript fully implement deep Clone objects? Ithub Lodash source MDN structured clone algorithm jQuery V3.2.1 source code