This article was first published on my personal website: Litgod.net

This is a classic interview question, I believe most of you have the experience of being asked by the interviewer, so how many ways can you implement deep copy and light copy? Is not asked to your knowledge blind 😊, that let us sum up the commonly used depth of copy (clone) method!

Prior to the start

Before we begin, we need to clarify JS data types and the concept of data stores (stacks and heaps) :

  • JS data types are divided into basic data types and reference data types (also known as complex data types).
Basic data types Reference data type
Number Object
String Function
Boolean Array
Undefind Date
Null RegExp
Symbol (new in ES6) Math
BigInt (new in ES10) . Are instance objects of type Object
  • Basic data types and reference data types are stored differently:

Basic data types: variable names and values are stored in stack memory;

Reference data types: Variable names are stored in stack memory, values are stored in heap memory, and heap memory provides a reference address to the value in heap memory, which is stored in stack memory.

Such as:

let obj = {
    a: 100.b: 'name'.c: [10.20.30].d: {x:10}},Copy the code

Obj is stored in memory as follows:

Stack memory Stack memory Heap memory
name val val
a 100
b ‘name’
c AAAFFF000 (a reference address that points to the value of heap memory) [10, 30]
d BBBFFF000 (a reference address that points to the value of heap memory) { x:10 }

Now that we have a basic understanding of these concepts, we’ll start talking about light copy.

Shallow copy

What is shallow copy? Obj2 is called a shallow copy of obj when it copies obj’s data and changes to obj2 cause changes to OBj.

For example 🌰 :

let obj = {
    a: '100',}let obj2 = obj;
obj2.a = '200';
console.log(obj.a)    / / '200'
Copy the code

When obj is directly assigned to obj2, the change in the A property in obj2 causes the change in the A property in obj.

Obj2 and OBj refer to the same storage address, so changes in obj2 will of course cause changes in OBj.

Common shallow copy

Take the following objects as an example:

let obj = {
    a: '100'.b: undefined.c: null.d: Symbol(2),
    e: /^\d+$/.f: new Date.g: true.arr: [10.20.30].school: {name:'cherry'
    },
    fn: function fn() {
        console.log('fn'); }}Copy the code

Method 1: Direct assignment

The direct assignment method, which is the example we just gave 🌰, is a pure shallow copy, and any changes to obj2 are reflected in obj.

Method two: Use object deconstruction

letobj2 = { ... obj }Copy the code

Method three: Use loops

Object loop We use a for in loop, but the for in loop iterates through the object’s inherited properties. We only need its private properties, so we can add hasOwnProperty to preserve the object’s private properties.

let obj2 = {};
for(let i in obj) {
    if(! obj.hasOwnProperty(i))break; // Continue also works here
    obj2[i] = obj[i];
}
Copy the code

Object.assign(target,source)

This is a new object method in ES6. See ES6 Object Method for more information.

let obj2 = {};
Object.assign(obj2,obj); // Copy obj to obj2
Copy the code

Shallow copy summary:

Method one: it is a pure shallow copy, and any changes to obj2 are reflected in OBj. Methods 2, 3, and 4: The first layer of “deep copy” can be implemented, but not multiple layers of deep copy. For example, if we change the value of obj2:

obj2.a = '200';
console.log(obj.a);  / / '100'
// the obj. A property is unchanged

obj2.school.name = 'susan';
console.log(obj.school.name);  // 'sucan'
// The obj.school.name property changes with obj2
Copy the code

These copying methods are not sufficient for deeper copying, so we need another fail-safe — deep copying.

Deep copy

Json.parse () and json.stringify

let obj2 = JSON.parse(JSON.stringify(obj));

obj2.schoole.name= 'susan';
console.log(obj.school.name); // 'cherry'
//obj does not change the property value, indicating that it is a deep copy
Copy the code

This method is relatively simple deep copy, when the object attribute type is relatively simple, we can take this method to fast deep copy.

However, when the type of object attribute is more complex, you will find that this method can achieve deep copy, but also a lot of holes, after running the above code:

  • Attributes whose value is undefined are lost after conversion;
  • Properties with a value of type Symbol are lost after conversion;
  • Properties whose values are RegExp objects become empty objects after the transformation;
  • Properties of function objects whose values are lost after conversion;
  • An attribute of a Date object becomes a string after the conversion;
  • Object constructor is discarded and all constructors point to Object;
  • Object throws an error.

For the last two pits, let’s test them briefly:

  • Object constructor is discarded and all constructors point to Object
// constructor
function person(name) {
    this.name = name;
}

const Cherry = new person('Cherry');

const obj = {
    a: Cherry,
}
const obj2 = JSON.parse(JSON.stringify(obj));

console.log(obj.a.constructor, obj2.a.constructor); // [Function: person] [Function: Object]

Copy the code
  • Object throws an error
const obj = {};
obj.a = obj;

const obj2 = JSON.parse(JSON.stringify(obj)); // TypeError: Converting circular structure to JSON
Copy the code

Do you feel a lot of holes? So friends in the use of deep copy in this way, or to pay attention to the next. The reason for this problem is related to the serialization rules for the json.stringify method, which are detailed in the json.stringify guide.

Json.stringify serialization rules are described in the following document for your reference:

For most simple values, JSON stringification has the same effect as toString(), except that the serialized result is always a string:

JSON.stringify(42); 42 “/ /”

JSON.stringify(“42”); // “”42″”(string with double quotes)

JSON.stringify(null); // “null”

JSON.stringify(true); // “true”

All safe JSON values (json-safe) can be used with json.stringify (…) Stringing. A safe JSON value is one that can be rendered in a valid JSON format.

For simplicity, let’s take a look at what an unsafe JSON value is. Undefined, function, symbol (ES6+), and objects that contain circular references (objects refer to each other before forming an infinite loop) do not conform to JSON structure standards and cannot be handled by other JSON-enabled languages.

JSON.stringify(…) Undefined, function, and symbol are automatically ignored when encountered in objects, and null is returned in arrays (to keep unit positions unchanged).

Such as:

JSON.stringify(undefined); //undefined

JSON.stringify(function(){}); //undefined

JSON.stringify([1,undefined,function(){},4]); //”[1, null, null, 4]”

JSON.stringify({a:2, b: function(){}}); //”{“a”: 2}”

Execute json.stringify (…) on objects that contain circular references ; Complains.

How to serialize json. stringify is also an interesting question to learn, because interviewers like to ask you no…

Method two: Handwritten deepClone

Since the first method has its drawbacks, the ultimate solution would be to hand-write a deepClone.

CloneDeep (cloneDeep, cloneDeep, cloneDeep, cloneDeep, cloneDeep, cloneDeep, cloneDeep, cloneDeep)

Simple implementation idea:

1. Traverse the copied object to check whether it is the original value. If yes, assign the value in shallow copy mode.

2. If reference values are used, filter special types one by one, and the case that reference values are arrays is compatible.

3. If the object to be copied contains the original value, the shallow copy can be realized. If there are reference values, the above series of judgments need to be repeated (recursive assignment).

How does this approach work in code?

let obj = {
    a: '100'.b: undefined.c: null.d: Symbol(2),
    e: /^\d+$/.f: new Date.g: true.arr: [10.20.30].school: {name: 'cherry',},fn: function fn() {
        console.log('fn'); }}function deepClone(obj) {
    // Filter all special cases null undefined date reg
    if (obj == null) return obj;  // null and undefined are not handled
    if (obj instanceof Date) return new Date(obj);
    if (obj instanceof RegExp) return new RegExp(obj);
    if (typeofobj ! = ='object') return obj;  // Return normal constants directly

    // The purpose of creating empty objects is not to create empty objects directly: the result of cloning remains the same class as before,
    // It is also compatible with arrays
    let newObj = new obj.constructor;
    for (const key in obj) {
        if (obj.hasOwnProperty(key)) {  // Do not copy the properties on the prototype chain
            newObj[key] = deepClone(obj[key]);  // Recursive assignment}}return newObj;
}
let obj2 = deepClone(obj);
console.log(obj2);
Copy the code

Execute the code, and the result of obj2 is the same as that of OBj, and the changes to the property values do not affect each other.

Q: Why does type null return object?

A: In js design, the first three digits of object are 000, and null is all 0 in 32-bit representation. Therefore, typeof NULL also prints object.

At this point, we have implemented a relatively simple deep copy of the code. If you can write the above implementation method during the interview, you should pass. However, in the face of complex, multi-type objects, the above method still has many defects.

For example, we add a Symbol attribute to the school object in obj:

//== New code ==
let s1 = Symbol('s1');

let obj = {
    a: '100'.b: undefined.c: null.d: Symbol(2),
    e: /^\d+$/.f: new Date.g: true.arr: [10.20.30].school: {name: 'cherry'.//== New code ==
        [s1]: 's1'
    },
    fn: function fn() {
        console.log('fn'); }}let obj2 = deepClone(obj);
console.log(obj2);
Copy the code

Symbol(s1): ‘s1’ in school is not copied successfully. To solve this problem, we can use the getOwnPrepertySymbols() method provided by Object to enumerate all properties of the Object whose key is symbol. See MDN for detailed instructions on how to use this property, or you can use reflect.ownkeys ().

For example, if the object we copied is referred to in a loop, deepClone will continue to execute, causing the stack to burst. For example:

let obj = {
    a: '100'.b: undefined.c: null.d: Symbol(2),
    e: /^\d+$/.f: new Date.g: true.arr: [10.20.30].school: {name: 'cherry',},fn: function fn() {
        console.log('fn');    
    }
}

obj.h = obj;

let obj2 = deepClone(obj);
console.log(obj2);
Copy the code

After executing the above code, the console throws a stack overflow error: Maximum Call Stack Size exceeded. In fact, the idea to solve the circular reference is to judge whether the current value already exists before assigning the value to avoid the circular reference. Here, we can use the WeakMap of ES6 to generate a hash table.

For these two problems, let’s optimize the code:

let s1 = Symbol('s1');

let obj = {
    a: '100'.b: undefined.c: null.d: Symbol(2),
    e: /^\d+$/.f: new Date.g: true.arr: [10.20.30].school: {name:'cherry',
        [s1]: 's1'
    },
    fn: function fn() {
        console.log('fn');    
    }
}

obj.h = obj;

function deepClone(obj, hash = new WeakMap()) {
    // Filter all special cases null undefined date reg
    if (obj == null) return obj;  //null and undefined are not handled
    if (obj instanceof Date) return new Date(obj);
    if (obj instanceof RegExp) return new RegExp(obj);
    if (typeofobj ! = ='object') return obj;  // Return normal constants directly
    
    // To prevent cyclic references from popping the stack, return the copied objects directly
    if (hash.has(obj)) return hash.get(obj);

    // The purpose of not creating an empty object directly: the result of cloning remains the same class as before
    // It is also compatible with arrays
    let newObj = new obj.constructor;

    hash.set(obj, newObj)  // Create a mapping table
    
    // Check if there is an attribute whose key is symbol
    let symKeys = Object.getOwnPropertySymbols(obj);
    if (symKeys.length) { 
        symKeys.forEach(symKey= > {
            newObj[symKey] = deepClone(obj[symKey], hash);   
        });
    }

    for (const key in obj) {
        if (obj.hasOwnProperty(key)) {  // Do not copy the properties on the prototype chain
            newObj[key] = deepClone(obj[key], hash);  // Recursive assignment}}return newObj;
}
let obj2 = deepClone(obj);
console.log(obj2);
Copy the code

In this way, a more complete deep copy is achieved

However, perfect but not perfect, there are higher dimension problems need to be optimized, such as: 1. There are still a lot of things we need to learn in order to achieve a perfect deep copy, not considering the copying of maps and sets in ES6

summary

If you are still interested in deep copy or want to research, you can read the lodash deep copy code, I believe you will have a further understanding of deep copy ~

conclusion

Welcome to pay attention to my public number, research front-end technology together, look forward to common progress with you.