Shallow vs. Deep Copying in JavaScript

Mark O'Neill
Share

Copying and modifying objects in JavaScript is never as simple as it seems. Understanding how objects and references work during this process is essential for web developers and can save hours of debugging. This becomes increasingly important when you work with large stateful applications like those built in React or Vue.

Shallow copying and deep copying refer to how we make copies of an object in JavaScript and what data is created in the ‘copy’. In this article, we’ll delve into the distinctions between these methods, explore their real-world applications, and uncover the potential pitfalls that can emerge when using them.

What is ‘Shallow’ Copying

Shallow copying refers to the process of creating a new object that is a copy of an existing object, with its properties referencing the same values or objects as the original. In JavaScript, this is often achieved using methods like Object.assign() or the spread syntax ({...originalObject}). Shallow copying only creates a new reference to the existing objects or values and doesn’t create a deep copy, which means that nested objects are still referenced, not duplicated.

Let’s look at the following code example. The newly created object shallowCopyZoo is created as a copy of zoo via the spread operator, which has caused some unintended consequences.

let zoo = {
  name: "Amazing Zoo",
  location: "Melbourne, Australia",
  animals: [
    {
      species: "Lion",
      favoriteTreat: "🥩",
    },
    {
      species: "Panda",
      favoriteTreat: "🎋",
    },
  ],
};

let shallowCopyZoo = { ...zoo };
shallowCopyZoo.animals[0].favoriteTreat = "🍖";
console.log(zoo.animals[0].favoriteTreat); 
// "🍖", not "🥩"

But let’s look at what is really in shallowCopyZoo. The properties name and location are primitive values (string), so their values are copied. However, the animals property is an array of objects, so the reference to that array is copied, not the array itself.

You can quickly test this (if you don’t believe me) using the strict equality operator (===). An object is only equal to another object if the refer to the same object (see Primitive vs. Reference data types). Notice how the property animals is equal on both but the objects themselves are not equal.

console.log(zoo.animals === shallowCopyZoo.animals)
// true

console.log(zoo === shallowCopyZoo)
// false

This can lead to potential issues in code bases and make life especially hard when working with large Modifying a nested object in the shallow copy also affects the original object and any other shallow copies, as they all share the same reference.

Deep Copying

Deep copying is a technique that creates a new object, which is an exact copy of an existing object. This includes copying all its properties and any nested objects, instead of references. Deep cloning is helpful when you need two separate objects that don’t share references, ensuring changes to one object don’t affect the other.

Programmers often use deep cloning when working with application state objects in complex applications. Creating a new state object without affecting the previous state is crucial for maintaining the application’s stability and implementing undo-redo functionality properly.

How to deep copy using JSON.stringify() and JSON.parse()

A popular and library-free way of deep copying is to use the built in JSON stringify() and parse() methods.

The parse(stringify()) method is not perfect. For example, special data types like Date will be stringified and undefined values will be ignored. Like all options in this article, it should be considered for your individual use case.

In the code below, we’ll create a deepCopy function these methods to deep clone an object. We then copy the playerProfile object and modify the copied object without affecting the original one. This showcases the value of deep copying in maintaining separate objects without shared references.

const playerProfile = {
  name: 'Alice',
  level: 10,
  achievements: [
    {
      title: 'Fast Learner',
      emoji: '🚀'
    },
    {
      title: 'Treasure Hunter',
      emoji: '💰'
    }
  ]
};

function deepCopy(obj) {
  return JSON.parse(JSON.stringify(obj));
}

const clonedProfile = deepCopy(playerProfile);

console.log(clonedProfile);
/* Output:
{
  name: 'Alice',
  level: 10,
  achievements: [
    {
      title: 'Fast Learner',
      emoji: '🚀'
    },
    {
      title: 'Treasure Hunter',
      emoji: '💰'
    }
  ]
}
*/

// Modify the cloned profile without affecting the original profile
clonedProfile.achievements.push({ title: 'Marathon Runner', emoji: '🏃' });
console.log(playerProfile.achievements.length); // Output: 2
console.log(clonedProfile.achievements.length); // Output: 3

Libraries for Deep Copying

There are also a variety of third-party libraries that offer a deep copying solution.

A Vanilla JS Deep Copy Function

If for some reason you do not want to use the JSON object or a third party library, you can also create a custom deep copy function in vanilla JavaScript. that recursively iterates through the object properties and creates a new object with the same properties and values.

const deepCopy = (obj) => {
    if (typeof obj !== 'object' || obj === null) {
        return obj;
    }

    const newObj = Array.isArray(obj) ? [] : {};

    for (const key in obj) {
        newObj[key] = deepCopy(obj[key]);
    }

    return newObj;
}

const deepCopiedObject = deepCopy(originalObject);

Downsides of Deep Copying

While deep copying offers great benefits for data accuracy, it’s recommended to evaluate whether deep copying is necessary for each specific use case. In some situations, shallow copying or other techniques for managing object references might be more suitable, providing better performance and reduced complexity.

  1. Performance impact: Deep copying can be computationally expensive, especially when dealing with large or complex objects. As the deep copy process iterates through all nested properties, it may take a significant amount of time, negatively impacting the performance of your application.
  2. Memory consumption: Creating a deep copy results in the duplication of the entire object hierarchy, including all nested objects. This can lead to increased memory usage, which may be problematic, particularly in memory-constrained environments or when dealing with large data sets.
  3. Circular references: Deep copying can cause issues when objects contain circular references (i.e., when an object has a property that refers back to itself, directly or indirectly). Circular references can lead to infinite loops or stack overflow errors during the deep copy process, and handling them requires additional logic to avoid these issues.
  4. Function and special object handling: Deep copying may not handle functions or objects with special characteristics (e.g., Date, RegExp, DOM elements) as expected. For example, when deep copying an object containing a function, the function’s reference might be copied, but the function’s closure and its bound context will not be duplicated. Similarly, objects with special characteristics might lose their unique properties and behavior when deep copied.
  5. Implementation complexity: Writing a custom deep copy function can be complex, and built-in methods like JSON.parse(JSON.stringify(obj)) have limitations, such as not handling functions, circular references, or special objects correctly. While there are third-party libraries like Lodash’s _.cloneDeep() that can handle deep copying more effectively, adding an external dependency for deep copying might not always be ideal.

Conclusion

Thanks for taking the time to read this article. Shallow vs. Deep copying is surprisingly more complex than any first timer imagines. Although there are a lot of pitfalls in each approach, taking the time to review and consider the options will ensure your application and data remains exactly how you want it to be.