JavaScript's Object Copy Puzzle: Shallow vs Deep?

JavaScript's Object Copy Puzzle: Shallow vs Deep?

Shallow Copy Vs Deep Copy

In the JavaScript world, you often experience some strange behavior. One such quirk that frequently confounds developers is the Object Copy Problem.

In the wild and woolly realm of JavaScript, you may encounter some rather peculiar phenomena. Picture this: you painstakingly copy an object, hoping to have a neat, independent duplicate. Yet, when you make changes to your copy, the original object laughs in the face of your efforts, morphing along with it.

But why does this puzzling problem persist, leaving developers scratching their heads in frustration?

Why Object Copy Problem Arrives in Javascript?

The Object Copy Problem emerges in JavaScript because of the way objects are managed in memory.

Unlike primitive data types such as numbers or strings, objects are assigned and passed by reference. This means that when you assign an object to a variable or pass it as an argument to a function, you're not creating a new, independent copy of that object. Instead, you're simply creating another reference to the same object in memory.

As a result, when you attempt to clone an object, you may unintentionally end up with multiple references pointing to the same underlying data. Modifying one reference will affect all other references to the same object.

This behavior can lead to unexpected side effects and bugs, as developers may assume that they are working with distinct copies of objects when they're modifying the original data.

What are the solutions for this Object Copy Problem?

The Object Copy Problem can be an annoying challenge for developers. Luckily, there are two main strategies to tackle this issue:

Shallow Copy

Imagine you want to create a copy of an object, but you're not interested in duplicating its nested properties. That's where shallow copying comes into play. With shallow copying, you create a new object and copy over the top-level properties from the original.

However, if the original object contains nested objects, only references to these nested objects are copied, not the objects themselves. This means changes made to nested objects in the cloned object will also affect the original.

Shallow copying can be achieved using the following methods:

  • Using Object.assign():

    Object.assign() is a built-in method in JavaScript that copies all enumerable own properties from one or more source objects to a target object.

      const originalObject = { a: 1, b: { c: 2 } };
      const shallowCopy = Object.assign({}, originalObject);
    
      shallowCopy.a = 3;
      shallowCopy.b.c = 4;
    
      console.log(originalObject); 
      // { a: 1, b: { c: 4 } }
      console.log(shallowCopy);    
      // { a: 3, b: { c: 4 } }
    

    Here, Object.assign() creates a shallow copy originalObject by copying its enumerable own properties to an empty object {}. However, modifications to the shallow copy affect the original object and its nested properties.

  • Using Spread Syntax (...):

    Spread syntax (...) offers a concise way to achieve shallow copies of objects.

      const originalObject = { a: 1, b: { c: 2 } };
      const shallowCopy = { ...originalObject };
    
      shallowCopy.a = 3;
      shallowCopy.b.c = 4;
    
      console.log(originalObject); 
      // { a: 1, b: { c: 4 } }
      console.log(shallowCopy);    
      // { a: 3, b: { c: 4 } }
    

    Similarly to Object.assign(), spread syntax creates a shallow copy of originalObject, but changes made to the shallow copy also impact the original object and its nested properties.

  • Using Lodash's _.clone():

    Lodash provides a convenient utility function, _.clone(), for shallow copying objects.

      const _ = require('lodash');
    
      const originalObject = { a: 1, b: { c: 2 } };
      const shallowCopy = _.clone(originalObject);
    
      shallowCopy.a = 3;
      shallowCopy.b.c = 4;
    
      console.log(originalObject); 
      // { a: 1, b: { c: 4 } }
      console.log(shallowCopy);    
      // { a: 3, b: { c: 4 } }
    

    Like the previous methods, _.clone() creates a shallow copy originalObject, resulting in shared references to nested objects and their properties.

In all examples, although the top-level properties of the objects are copied, changes to nested objects in the shallow copies affect the original objects, demonstrating the shallow nature of the copies.

This leads us to a second and better solution for this problem.

Deep Copy

Now, let's say you need a completely independent copy of an object, including all its nested properties.

When it comes to creating a truly independent duplicate of an object and all its nested properties, deep copying is the way to go. With deep copying, you carefully create a new object and recursively copy all nested objects and their properties. This ensures that the cloned object is entirely separate from the original, preventing any unintended side effects.

Deep copying can be achieved using the following methods:

  • Using a Custom Recursive Function:

      function deepCopy(obj) {
          if (typeof obj !== 'object' || obj === null) {
              return obj;
          }
          const newObj = Array.isArray(obj) ? [] : {};
          for (let key in obj) {
              newObj[key] = deepCopy(obj[key]);
          }
          return newObj;
      }
    
      const originalObject = { a: 1, b: { c: 2 } };
      const deepCopyObject = deepCopy(originalObject);
    
      deepCopyObject.a = 3;
      deepCopyObject.b.c = 4;
    
      console.log(originalObject); 
      // { a: 1, b: { c: 2 } }
      console.log(deepCopyObject); 
      // { a: 3, b: { c: 4 } }
    

    The deepCopy() function recursively traverses the input object and its nested properties, creating a new object with completely independent copies of all properties.

  • Using Lodash's _.cloneDeep():

    Lodash provides a convenient utility function, _.clone(), for shallow copying objects.

      const _ = require('lodash');
    
      const originalObject = { a: 1, b: { c: 2 } };
      const deepCopyObject = _.cloneDeep(originalObject);
    
      deepCopyObject.a = 3;
      deepCopyObject.b.c = 4;
    
      console.log(originalObject); 
      // { a: 1, b: { c: 2 } }
      console.log(deepCopyObject); 
      // { a: 3, b: { c: 4 } }
    

    Lodash's _.cloneDeep() function handles deep copying effortlessly by recursively copying all nested properties of an object, ensuring complete independence from the original object. It uses the same method as the previous one but we don't need to write a recursive function to do a deep copy.

  • Using JSON Serialization/Deserialization:

      const originalObject = { a: 1, b: { c: 2 } };
      const deepCopyObject = JSON.parse(JSON.stringify(originalObject));
    
      deepCopyObject.a = 3;
      deepCopyObject.b.c = 4;
    
      console.log(originalObject); 
      // { a: 1, b: { c: 2 } }
      console.log(deepCopyObject); 
      // { a: 3, b: { c: 4 } }
    

    JSON serialization and deserialization provide a simple yet effective way to achieve deep copying.

    By converting the original object to a JSON string and then parsing it back into a new object, all references to the original object and its nested properties are broken, resulting in a deep copy.

In each example, modifications to the deep copy do not affect the original object or its nested properties, demonstrating the effectiveness of deep copying in JavaScript

Which Solution is preferable?

The preference between deep copy and shallow copy depends on the specific context and requirements of your application.

When you only need a surface-level copy of the object, and your priority is performance and memory efficiency, shallow copying is the way to go. However, if you want to copy the entire object along with its nested properties, deep copying is preferable over shallow copying. shallow copying offers efficiency when a surface-level copy suffices, while deep copying ensures completeness when you need to replicate the object with all its nested properties.

In conclusion, while shallow copying offers efficiency in terms of memory and performance, it comes with the caveat that changes to nested objects impact both the original and copied objects. On the other hand, deep copying ensures complete isolation between objects but may be resource-intensive. Choosing between shallow and deep copying depends on the specific requirements of your application and the level of independence needed between the original and copied objects.