The Battle of Copying Objects in JavaScript: Shallow Copy vs. Deep Copy

The Battle of Copying Objects in JavaScript: Shallow Copy vs. Deep Copy

Photo by svklimkin on Unsplash

Introduction

Copying objects in JavaScript can be a challenging task, especially when dealing with nested structures. As JavaScript developers, it's crucial to have a clear understanding of shallow copy and deep copy. Knowing the difference between the two can greatly impact your code's performance. In this article, we will explore the key disparities between shallow copy and deep copy in JavaScript, their workings, and when to utilize them. By the end of this article, you will have a comprehensive understanding of this topic and how to effectively implement it in your code.

Why Copying Matters?

In JavaScript, when we assign an object to a new object, the objects are not copied by value but by reference. This means that any changes made to a copied object can also affect the original. This is where copying becomes crucial, and there are two main types: shallow copy and deep copy. Let's dive into understanding them:

What is Shallow Copy?

In JavaScript, shallow copy involves creating a new object that contains the same values as the original. However, the new object only holds references to the original values, not the values themselves.

To illustrate this concept, consider the following code snippet:

let originalObject = {
    name: "John",
    age: 20
}

let copiedObject = originalObject;

console.log(originalObject);    // { name: "John", age: 20 }
console.log(copiedObject);     //  { name: "John", age: 20 }

In the code snippet above, objects are copied by reference, not by their actual values. Let's modify the name of the copied object and further comprehend the concept:

1) Example Of Shallow Copy

copiedObject.name = "Peter";

console.log(originalObject);    // { name: "Peter", age: 20 }
console.log(copiedObject);     //  { name: "Peter", age: 20 }

Here, we can observe that the name of the original object is also updated. This demonstrates that the copied object references the original object's memory address. To overcome this issue with shallow copying, we have an alternative known as "Deep Copy."

What is Deep Copy?

Unlike shallow copy, deep copy creates an entirely new object with all the values of the original. In other words, deep copy generates an independent copy of the object.

To achieve a deep copy of an object, we can use various methods such as JSON.parse() and JSON.stringify(), the spread operator, the Object. assign() method, or third-party libraries like Lodash. However, it is important to understand the limitations of each method. Let's examine each method with code examples:

  1. Using The JSON.stringify() method

let originalObject = {
   name: "Kate",
   age: 30
 }

let copiedObject = JSON.parse(JSON.stringify(originalObject));

console.log(originalObject);  // { name: "Kate", age: 30 }
console.log(copiedObject);   //  { name: "Kate", age: 30 }

copiedObject.name = "Sarah";

console.log(originalObject);  // { name: "Kate", age: 30 }
console.log(copiedObject);   //  { name: "Sarah", age: 30 }

In this example, we can observe that only the copied object's value is modified. However, a limitation arises when we have a function type in our object. Additionally, we need to use JSON.parse to convert the string type back into an object. For instance:

// Example also includes case for nested objects
let originalObject = {
    name: "James",
    age: "22",
    getName: function() {
        return this.name;
    },
    address: {
        city: "New York",
        country: "USA"
    }
}
let copiedObject = JSON.parse(JSON.stringify(originalObject));

console.log(originalObject);  // { name: "James", age: 22, getName: f(), address:{ city: "New York", country: "USA" } }
console.log(copiedObject);   //  { name: "James", age: 22, address:{ city: "New York", country: "USA" } }

copiedObject.name = "Sarah";
copiedObject.address.city = "New Jersey";

console.log(originalObject);  // { name: "James", age: 22, getName:{}, address:{ city: "New York", country: "USA" } }
console.log(copiedObject);   //  { name: "Sarah", age: 22, address:{ city: "New Jersey", country: "USA" } }

In this case, we can see that JSON.stringify doesn't copy the function type in the copied object, but it performs a deep copy. To overcome this limitation, we can utilize the Object. assign() method.

  1. Using the Object.assign() Method (before ES6)

// Example also includes case for nested object 
let originalObject = {
    name: "James",
    age: "22",
    getName: function() {
        return this.name;
    },
    address: {
        city: "New York",
        country: "USA"
    }
}
let copiedObject = Object.assign({}, originalValue);

console.log(originalObject);  // { name: "James", age: 22, getName: f(), address:{ city: "New York", country: "USA" } }
console.log(copiedObject);   //  { name: "James", age: 22, getName: f(), address:{ city: "New York", country: "USA" } }

copiedObject.name = "Sarah";
copiedObject.address.city = "New Jersey";

console.log(originalObject);  // { name: "James", age: 22, getName:{}, address:{ city: "New Jersey", country: "USA" } }
console.log(copiedObject);   //  { name: "Sarah", age: 22, getName:{}, address:{ city: "New Jersey", country: "USA" } }

In this example, the Object. assign() method solves the limitation of copying the function type. However, since Object. assign() only performs a partial deep copy in case of nested structures, we can employ the spread operator. Nevertheless, the spread operator also performs a partial copy by default.

  1. Using the Spread Operator

  2.    let originalObject = {
           name: "Clisha",
           age: "22",
           getName: function() {
               return this.name;
           },
           address: {
               city: 'Delhi',
               country: 'India'
           }
       }
    
       let copiedObject = {...originalObject};
    
       copiedObject.name = "Katy";
       copiedObject.address.city = "Pune";
    
       console.log(originalObject);    // { name: "Clisha", age: 22, getName: f(), address: { city: "Pune", country: "India" } }
       console.log(copiedObject);     // { name: "Katy", age: 22, getName: f(), address: { city: "Pune", country: "India" } }
    

    Here, we can see that the original object's city value is also updated. Thus, the spread operator only performs a partial copy for nested objects. To address this issue, we need to spread the keys first and then copy them.

  3.      copiedObject = {
             ...copiedObject,
             name: "Alisha",
             address: {
                 ...copiedObject.address,
                 city: "Goa"
             }
         }
    
         console.log(originalObject); // { name: "Clisha", age: 22, getName: f(), address: { city: "Delhi", country: "India" } }
         console.log(copiedObject);   // // { name: "Alisha", age: 22, getName: f(), address: { city: "Goa", country: "India" } }
    

    By spreading the keys first, we successfully perform a deep copy of the object. Another method involves using a third-party library. Assuming Lodash is already installed, we can leverage it.

  4. Using a third-party library like Lodash

  5.    const _ = require("lodash");
       let originalObject = {
           name: "Clisha",
           age: "22",
           getName: function() {
               return this.name;
           },
           address: {
               city: 'Delhi',
               country: 'India'
           }
       }
       let copiedObject = _.cloneDeep(originalObject);
    
       copiedObject.name = "James";
       copiedObject.address.city = "Mumbai";
    
       console.log(originalObject); // { name: "Clisha", age: 22, getName: f(), address: { city: "Delhi", country: "India" } }
       console.log(copiedObject);   // { name: "James", age: 22, getName: f(), address: { city: "Mumbai", country: "India" } }
    

By utilizing Lodash, we can overcome all the limitations faced by other methods when performing a deep copy.

Key Differences Between Shallow Copy & Deep Copy

Now that we understand the concepts of shallow copy and deep copy, let's explore the key differences between them:

  1. Data Duplication: Shallow copy only contains a reference to the original object, whereas deep copy creates an entirely new object with all the values

  2. Object References: Shallow copy merely copies the object's reference, which means any changes made to the original object will reflect in the new object. Conversely, deep copy generates a new object independent of the original.

  3. Performance: Since shallow copy only copies references, it is faster and requires less memory compared to deep copy, which creates a new object entirely.

Which One To Choose?

The decision to use shallow copy or deep copy depends on the specific use case and requirements of your application. If you only need to modify the top-level properties or elements of an object, a shallow copy might suffice. However, if you need to modify nested objects or ensure the original object remains unchanged, a deep copy is the way to go.

Conclusion

Copying objects in JavaScript can be a challenging task, but understanding the difference between a shallow copy and a deep copy is crucial to avoid unexpected behaviour and bugs in your applications. Shallow copying can provide a quick and easy solution but can lead to unexpected behaviour when dealing with complex data structures. Deep copying is more powerful and helps maintain data integrity, albeit at a higher computational cost. Ultimately, the choice between shallow copy and deep copy depends on the specific use case and requirements of your application, and various techniques discussed in this blog can be implemented.

Thanks for Reading! 🙌
See you in the next blog! Until then, keep learning and sharing.

Let’s connect: