Deep copy and shallow copy

Deep copy and shallow copy

1.1 What is a deep copy? What is a shallow copy?

Deep copy means that the source object and the copied object are independent of each other, and the change of any one of them will not affect the other object. For example, a person named Zhang San, and later copied with him (assuming the law allows) another person named Li Si, whether it is Zhang San lacking arms and legs or Li Si lacking arms and legs will not affect the other person. This is a deep copy. A typical deep copy is JavaScript's "value types" (7 data types), such as string, number, bigint, boolean, null, undefined, and symbol.

The copy other than the deep copy is called the shallow copy.

For the sake of saving memory, JavaScript's copy of "reference type" (that is, the eighth data type) Object is a shallow copy by default.

2. Several simple deep copies

2.1 JSON built-in methods

(() => {
    let a = { x: 1 }
    let b = JSON.parse(JSON.stringify(a))
    console.log(b)//>> {x:1}
    b.x = 2
    console.log(b)//>> {x:2}
    console.log(a)//>> {x:1}
})();

The method is to use JSON.stringify to serialize the object into a string, and then use JSON.parse to parse the json string into an object. When parsing, it will construct a new memory address to store the new object.

shortcoming:

  • Will ignore undefined;

  • Symbol will be ignored;

  • If the attribute of the object is Function, because the JSON format string does not support Function, it will be automatically deleted during serialization;

  • Such as Map, Set, RegExp, Date, ArrayBuffer and other built-in types will be lost during serialization;

  • The copy of circular reference objects is not supported.

2.2 Object's built-in method assign

(() => {
    let a = { x: 1 }
    let b = Object.assign({},a);
    console.log(b)//>> {x:1}
    b.x = 2
    console.log(b)//>> {x:2}
    console.log(a)//>> {x:1}
})();

This method is to use Object.assign to splice objects, insert the content of subsequent objects into the object specified by the first parameter, and will not modify the object after the first parameter, but we specify the first object as an anonymous empty Object to achieve deep copy.

The Object.assign method will only copy the enumerable properties of the source object itself to the target object.

shortcoming:

  • If the nesting level of the object exceeds 2 levels, a shallow copy will appear;

  • Non-enumerable attributes cannot be copied.

2.3 Use MessageChannel

// Undefined + circular reference
let obj = {
  a: 1,
  b: {
    c: 2,
    d: 3,
  },
  f: undefined
}

obj.c = obj.b;
obj.e = obj.a;
obj.b.c = obj.c;
obj.b.d = obj.b;
obj.b.e = obj.b.c;

function deepCopy(obj) {
  return new Promise((resolve) => {
    const {port1, port2} = new MessageChannel();
    port2.onmessage = ev => resolve(ev.data);
    port1.postMessage(obj);
  });
}

deepCopy(obj).then((copy) => {// Asynchronous
    let copyObj = copy;
    console.log(copyObj, obj)
    console.log(copyObj == obj)
});

shortcoming:

  • This method is asynchronous;

  • When copying an object with a function, an error will still be reported.

3.3 Recursive deep copy

function deepCopy(obj) {
    // Create a new object
    let result = {}
    let keys = Object.keys(obj),
        key = null,
        temp = null;

    for (let i = 0; i < keys.length; i++) {
        key = keys[i];    
        temp = obj[key];
        // Recursive operation if the value of the field is also an object
        if (temp && typeof temp === 'object') {
            result[key] = deepCopy(temp);
        } else {
        // Otherwise directly assign to the new object
            result[key] = temp;
        }
    }
    return result;
}

3.4 Deep copy of circular reference

For example, in this case, obj refers to itself:

var obj = {
    name: 'coffe1989',
    sex: 'male'
};
obj['deefRef'] = obj;

At this time, if the above deepCopy function is called, it will fall into an infinite loop, which will cause the stack to overflow. Solving this problem is also very simple. You only need to determine whether the field of an object refers to this object or any parent of this object. Modify the code:

function deepCopy(obj, parent = null) {
// Create a new object
let result = {};
let keys = Object.keys(obj),
key = null,
temp = null,
_parent = parent;
// If the field has a parent, you need to trace the parent of the field
while (_parent) {
// If the field refers to its parent, it is a circular reference
if (_parent.originalParent === obj) {
// Circular reference directly returns a new object at the same level
return _parent.currentParent;
}
_parent = _parent.parent;
}
for (let i = 0; i <keys.length; i++) {
key = keys[i];
temp = obj[key];
// If the value of the field is also an object
if (temp && typeof temp ==='object') {
// Perform deep copy recursively, pass the same-level objects to be copied and new objects to parent to facilitate tracing circular references
result[key] = DeepCopy(temp, {
originalParent: obj,
currentParent: result,
parent: parent
});
} else {
result[key] = temp;
}
}
return result;
}

3.5 The ultimate perfect solution: lodash

lodash's _.cloneDeep() supports loop objects and a large number of built-in types. It handles many details well and is recommended. So, how does lodash solve the problem of circular references? Check the source code:

1.3.1.6.png

You will find that lodash uses a stack to record all the copied reference values. If the same reference value is encountered again, it will not copy it again, but will use the previously copied one. In summary, in a production environment, we recommend using lodash's cloneDeep().