How to deep clone an object using Lodash

How to deep clone an object using Lodash

When you copy an object in JavaScript, the default behavior is to create a shallow copy. This means that only the first level of the object is duplicated. Any nested objects or arrays are still referenced, not cloned. It sounds simple, but this subtlety can lead to some seriously unexpected bugs if you’re not careful.

Consider an object with nested structures:

const original = {
  name: "Alice",
  address: {
    city: "Wonderland",
    zip: "12345"
  }
};

Now, if you do a shallow copy like this:

const shallowCopy = { ...original };

You might think you’ve created a completely new object, but look what happens when you mutate a nested property:

shallowCopy.address.city = "Looking Glass";

console.log(original.address.city); // Outputs: "Looking Glass"

That’s because address is still the same object referenced in both original and shallowCopy. Changing it in one place affects the other. That is a classic pitfall that’s easy to overlook, especially when your data structures start getting more complex.

Even methods like Object.assign() or the spread operator, which are often recommended for copying objects, only perform shallow copies. They are handy for cloning flat objects or when you explicitly want to share nested references, but they’re dangerous if you expect a deep clone.

Arrays behave the same way:

const arr = [1, 2, [3, 4]];

const shallowArrCopy = [...arr];

shallowArrCopy[2][0] = 99;

console.log(arr[2][0]); // Outputs: 99

Here, the nested array is not cloned but shared. The moment you mutate the nested array in the copy, the original changes too. This can break assumptions in your code and lead to hard-to-trace bugs.

Functions, primitives, and symbols don’t have this problem because primitives get copied by value and functions are references that usually don’t change state internally. But any object, array, or nested structure needs special attention.

To avoid these pitfalls, you need a strategy that creates a true deep copy—one where every nested object or array is recursively duplicated. That’s where libraries like Lodash come in handy, with their cloneDeep method. It’s battle-tested and handles edge cases that a naive recursive function might miss, like circular references or special object types.

Before diving into Lodash’s approach, here’s a quick demonstration of why a naive deep clone function can also fail:

function naiveDeepClone(obj) {
  if (obj === null || typeof obj !== "object") {
    return obj;
  }

  if (Array.isArray(obj)) {
    return obj.map(naiveDeepClone);
  }

  const copy = {};
  for (const key in obj) {
    copy[key] = naiveDeepClone(obj[key]);
  }
  return copy;
}

const circular = {};
circular.self = circular;

const clone = naiveDeepClone(circular); // This will cause a stack overflow

Without handling circular references, a simple recursive deep clone will blow up. Lodash’s cloneDeep manages this gracefully and much more.

Mastering lodash’s cloneDeep method for perfect clones

Lodash’s cloneDeep method is a robust solution designed to handle deep cloning with all the tricky edge cases covered. It supports cloning arrays, objects, buffers, typed arrays, maps, sets, and even handles circular references without blowing the call stack.

Here’s how you can use cloneDeep in your project:

import cloneDeep from 'lodash/cloneDeep';

const original = {
  name: "Alice",
  address: {
    city: "Wonderland",
    zip: "12345"
  },
  hobbies: ["reading", "gardening"],
  meta: new Map([["key", "value"]])
};

const deepCopy = cloneDeep(original);

deepCopy.address.city = "Looking Glass";
deepCopy.hobbies.push("chess");
deepCopy.meta.set("key", "newValue");

console.log(original.address.city); // Outputs: "Wonderland"
console.log(original.hobbies.length); // Outputs: 2
console.log(original.meta.get("key")); // Outputs: "value"

Notice that after mutating the nested properties and collections in deepCopy, the original remains untouched. That’s the power of a true deep clone.

Under the hood, cloneDeep uses a combination of internal helper functions and a Stack data structure to keep track of all objects it has already cloned. This prevents infinite recursion when it encounters circular references.

If you want to see the difference in behavior with circular objects, try this:

const circular = {};
circular.self = circular;

const copy = cloneDeep(circular);

console.log(copy.self === copy); // Outputs: true
console.log(copy.self === circular); // Outputs: false

Here, copy.self references itself, preserving the circular structure in the clone, but it’s a completely separate object from the original circular. That’s impossible to achieve with naive cloning functions.

Another feature worth mentioning is that cloneDeep preserves the prototype chain of objects. If you have instances of custom classes or built-in types, their prototypes remain intact after cloning:

class Person {
  constructor(name) {
    this.name = name;
  }
  greet() {
    return Hello, ${this.name}!;
  }
}

const alice = new Person("Alice");
const aliceClone = cloneDeep(alice);

console.log(aliceClone instanceof Person); // true
console.log(aliceClone.greet()); // "Hello, Alice!"

This ensures that methods and behaviors attached to prototypes aren’t lost during cloning, another common shortcoming of many deep copy implementations.

While cloneDeep is powerful, keep in mind it’s not always the fastest option. Deep cloning is inherently more expensive than shallow copying, so use it judiciously, especially in performance-critical code or large data structures.

For those who want to avoid the full weight of Lodash, some modern alternatives and native browser APIs like structuredClone have emerged, but they don’t yet provide the same level of compatibility and flexibility. Until then, cloneDeep remains the go-to tool for safe, reliable deep cloning in JavaScript.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *