DEV Community

Cover image for Understanding Shallow Copy vs Deep Copy in JavaScript
Rowland
Rowland

Posted on

Understanding Shallow Copy vs Deep Copy in JavaScript

Introduction

If you've ever worked with objects or arrays in JavaScript, you might have run into unexpected mutations. You modify what you thought was a copy, only to find that the original data has changed too. This behavior can lead to subtle bugs that are hard to track down.

The root cause of this problem lies in how JavaScript handles references to complex data types. When you work with objects and arrays, you're often working with references rather than actual copies of the data. Understanding the difference between shallow and deep copying is crucial for writing predictable, bug-free code.

In this guide, we'll explore what shallow and deep copies are, how to create them, when to use each approach, and common pitfalls to avoid when copying data structures in JavaScript.


Methods of Copying in JavaScript?

In JavaScript, there are two main approaches to copying objects and arrays:

Shallow Copy creates a new object or array, but only copies the top-level properties. Any nested objects or arrays inside it are still referenced rather than copied. This means that if you modify a nested structure in the copied version, it also affects the original.

Deep Copy creates an entirely new object or array by recursively copying all nested structures. This results in a fully independent clone where changes in the copied structure do not affect the original at all.

Shallow Copy Techniques

A shallow copy creates a new object or array at the top level, but nested objects or arrays remain as references to the original data. Let's explore the different ways to create shallow copies.

Shallow Copying Objects

There are two primary methods for shallow copying objects:

Using the Spread Operator (...)

The spread operator is the most modern and concise way to create a shallow copy of an object:

const originalUser = {
  name: "John",
  age: 30,
  address: {
    city: "New York",
    zip: "10001"
  }
};

const shallowCopy = { ...originalUser };

// Modifying top-level property (safe)
shallowCopy.name = "Bob";
console.log(originalUser.name); // "John" - unchanged

// Modifying nested property (affects original!)
shallowCopy.address.city = "Los Angeles";
console.log(originalUser.address.city); // "Los Angeles" - changed!
Enter fullscreen mode Exit fullscreen mode

Using Object.assign()

The Object.assign() method copies all enumerable properties from one or more source objects to a target object:

const originalUser = {
  name: "John",
  age: 30,
  address: {
    city: "New York",
    zip: "10001"
  }
};

const shallowCopy = Object.assign({}, originalUser);

// Same behavior as spread operator
shallowCopy.age = 35; // Safe - only affects copy
shallowCopy.address.zip = "90001"; // Affects original!
Enter fullscreen mode Exit fullscreen mode
Shallow Copying Arrays

Arrays can also be shallow copied using several methods:

Using the Spread Operator (...)

const originalArray = [1, 2, [3, 4]];
const shallowCopy = [...originalArray];

// Modifying top-level element (safe)
shallowCopy[0] = 99;
console.log(originalArray[0]); // 1 - unchanged

// Modifying nested array (affects original!)
shallowCopy[2][0] = 99;
console.log(originalArray[2][0]); // 99 - changed!
Enter fullscreen mode Exit fullscreen mode

Using slice() Method

const originalArray = [1, 2, [3, 4]];
const shallowCopy = originalArray.slice();

// Same shallow copy behavior
Enter fullscreen mode Exit fullscreen mode

Using concat() Method

const originalArray = [1, 2, [3, 4]];
const shallowCopy = [].concat(originalArray);

// Same shallow copy behavior
Enter fullscreen mode Exit fullscreen mode

Deep Copy Techniques

A deep copy recursively copies all nested structures, creating a fully independent clone. Let's explore the methods available for deep copying.

Using structuredClone()

The structuredClone() method is a modern, built-in way to create deep copies. It works with both objects and arrays and handles most data types correctly:

const originalUser = {
  name: "John",
  age: 30,
  address: {
    city: "New York",
    zip: "10001"
  },
  hobbies: ["reading", "gaming"]
};

const deepCopy = structuredClone(originalUser);

// Modifying nested properties (safe)
deepCopy.address.city = "Los Angeles";
deepCopy.hobbies.push("cooking");

console.log(originalUser.address.city); // "New York" - unchanged
console.log(originalUser.hobbies); // ["reading", "gaming"] - unchanged
Enter fullscreen mode Exit fullscreen mode

structuredClone() also works seamlessly with arrays:

const originalArray = [1, 2, { name: "John" }, [3, 4]];
const deepCopy = structuredClone(originalArray);

deepCopy[2].name = "Bob";
deepCopy[3][0] = 99;

console.log(originalArray[2].name); // "John" - unchanged
console.log(originalArray[3][0]); // 3 - unchanged
Enter fullscreen mode Exit fullscreen mode
Using Lodash _.cloneDeep()

If you need to support older browsers or have more complex cloning requirements, Lodash's _.cloneDeep() method is a reliable third-party solution:

// First, install lodash with npm install lodash before importing
import _ from 'lodash';

const originalData = {
  name: "John",
  settings: {
    theme: "dark",
    notifications: {
      email: true,
      push: false
    }
  }
};

const deepCopy = _.cloneDeep(originalData);

deepCopy.settings.notifications.email = false;
console.log(originalData.settings.notifications.email); // true - unchanged
Enter fullscreen mode Exit fullscreen mode

When to Use Each Approach

Choosing between shallow and deep copy depends on your data structure and use case:

Use Shallow Copy when:

  • You only need to copy the first level of an object or array
  • Your data structure doesn't contain nested objects or arrays
  • You want better performance (shallow copies are faster)
  • You're working with simple data structures like configuration objects with primitive values

Use Deep Copy when:

  • You're working with nested objects or arrays

  • You need complete independence from the original data

  • You're implementing features like undo/redo functionality

  • You're managing state in applications where mutations could cause bugs

  • You need to clone complex data structures without side effects

Common Pitfalls to Avoid

Even experienced developers can fall into traps when copying objects and arrays. Here are some common pitfalls and how to avoid them.

(1) Using Assignment Instead of Copying

The most common mistake is thinking that assignment (=) creates a copy:

const original = { name: "John", age: 30 };
const copy = original; // This is NOT a copy!

copy.name = "Bob";
console.log(original.name); // "Bob" - both variables point to the same object!
Enter fullscreen mode Exit fullscreen mode

Fix: Always use proper copying methods like spread operator or Object.assign():

const original = { name: "John", age: 30 };
const copy = { ...original }; // Now it's a real copy

copy.name = "Bob";
console.log(original.name); // "John" - unchanged
Enter fullscreen mode Exit fullscreen mode
(2) Using Shallow Copy for Nested Structures

Using shallow copy methods when you actually need a deep copy leads to unexpected mutations:

const user = {
  name: "John",
  preferences: {
    theme: "dark",
    language: "en"
  }
};

const updatedUser = { ...user }; // Shallow copy
updatedUser.preferences.theme = "light"; // Oops! Affects original

console.log(user.preferences.theme); // "light" - unexpected change!
Enter fullscreen mode Exit fullscreen mode

Fix: Use structuredClone() or _.cloneDeep() for nested structures:

const user = {
  name: "John",
  preferences: {
    theme: "dark",
    language: "en"
  }
};

const updatedUser = structuredClone(user); // Deep copy
updatedUser.preferences.theme = "light";

console.log(user.preferences.theme); // "dark" - original unchanged
Enter fullscreen mode Exit fullscreen mode
(3) Limitations of structuredClone()

While structuredClone() is powerful, it has limitations. It cannot clone:

  • Functions
  • DOM nodes
  • Symbols
  • Objects with getters/setters
const obj = {
  name: "John",
  greet: function() { return "Hello"; }, // Function won't be cloned
  symbol: Symbol("id") // Symbol won't be cloned
};

const clone = structuredClone(obj);
console.log(clone.greet); // undefined - function was not cloned
Enter fullscreen mode Exit fullscreen mode

Fix: For objects with functions, use _.cloneDeep() from Lodash or implement custom cloning logic.

(4) Using JSON.parse(JSON.stringify()) for Deep Copying

Some developers use JSON.parse(JSON.stringify(obj)) as a quick deep copy solution, but this approach has significant drawbacks:

const obj = {
  name: "John",
  date: new Date(),
  greet: function() { return "Hello"; },
  undef: undefined
};

const clone = JSON.parse(JSON.stringify(obj));

console.log(clone.date); // String, not a Date object
console.log(clone.greet); // undefined - function was lost
console.log(clone.undef); // undefined - property was removed
Enter fullscreen mode Exit fullscreen mode

This method fails with:

  • Functions (completely removed)
  • undefined values (removed)
  • Date objects (converted to strings)
  • Infinity and NaN (converted to null)
  • Circular references (throws error)

Fix: Use structuredClone() or _.cloneDeep() instead for reliable deep copying.

Conclusion

Understanding the difference between shallow and deep copying is essential for writing reliable JavaScript code. While shallow copies are sufficient for simple, flat data structures, deep copies are necessary when working with nested objects and arrays to ensure complete independence from the original data.

The key takeaways are:

  • Shallow copies only duplicate the top level, leaving nested structures as references to the original data
  • Deep copies recursively duplicate all levels, creating fully independent clones
  • Use structuredClone() for modern deep copying needs, or _.cloneDeep() for more complex scenarios
  • Always be aware of which copying method you're using and whether it matches your data structure

By mastering these copying techniques, you'll avoid common bugs related to unexpected mutations and write more predictable, maintainable code. Whether you're managing application state, implementing undo functionality, or simply working with complex data structures, knowing when and how to properly copy your data is a fundamental skill that will serve you throughout your JavaScript journey.

Top comments (0)