How to deep clone complex objects in JavaScript

How to deep clone complex objects in JavaScript

When you copy objects in JavaScript, understanding the difference between shallow and deep cloning especially important. Shallow cloning creates a new object, but it only copies the references of nested objects rather than duplicating them. This means if you have an object containing other objects or arrays, those nested structures aren’t truly copied; both original and clone end up pointing to the same inner objects.

Consider this example:

const original = {
  name: "Joel",
  preferences: {
    theme: "dark",
    notifications: true
  }
};

const shallowCopy = Object.assign({}, original);
shallowCopy.preferences.theme = "light";

console.log(original.preferences.theme); // "light"

Notice how changing the theme in the shallow copy also affected the original. This happens because preferences is a nested object, and Object.assign() only copies the reference to it, not the object itself.

The same problem arises with the spread syntax, which is often mistaken for a deep cloning method:

const spreadCopy = { ...original };
spreadCopy.preferences.notifications = false;

console.log(original.preferences.notifications); // false

Again, the nested object inside the copy and the original both point to the same reference. This behavior is a common pitfall that can lead to bugs, especially when you mutate data thinking you’re working on a separate copy.

Shallow cloning is fine for objects that contain only primitive values, but the moment you introduce arrays, objects, or functions inside, you need to be careful. Arrays behave exactly the same way – their contents aren’t duplicated, just the reference.

Here’s an example with an array:

const arrOriginal = [1, 2, { a: 3 }];
const arrCopy = arrOriginal.slice();
arrCopy[2].a = 42;

console.log(arrOriginal[2].a); // 42

slice() creates a new array but doesn’t deep clone the nested object inside it. So modifications to nested elements will reflect back to the original array.

Functions inside objects aren’t cloned either; they’re just copied by reference, but since functions are immutable, this is usually less of a concern. Still, it’s worth remembering that the copy points to the exact same function.

Shallow cloning works for simple, flat objects but becomes problematic the moment you deal with nested structures. Without a true deep clone, you risk subtle bugs where changing data in one place unexpectedly mutates data elsewhere, breaking the principle of immutability that many modern applications rely on.

Techniques for deep cloning using JSON methods

One of the simplest ways to achieve a deep clone in JavaScript is by using JSON.stringify() combined with JSON.parse(). This method serializes the entire object structure into a JSON string and then parses it back into a new object, effectively duplicating nested objects and arrays.

Here’s how it looks in practice:

const deepClone = JSON.parse(JSON.stringify(original));
deepClone.preferences.theme = "light";

console.log(original.preferences.theme); // "dark"

Notice that modifying the theme property on the cloned object does not affect the original. This works because the nested objects are completely copied, not just their references.

However, this approach comes with some significant caveats. Since the process involves converting the object to JSON, any properties that cannot be represented in JSON will be lost or corrupted. This includes functions, undefined, Symbol, and special object types like Date, Map, Set, RegExp, and custom class instances.

For example, consider this object:

const obj = {
  num: 1,
  func: () => console.log("hello"),
  date: new Date(),
  undef: undefined,
  nested: {
    sym: Symbol("sym")
  }
};

const clone = JSON.parse(JSON.stringify(obj));
console.log(clone);
/* Output:
{
  num: 1,
  nested: {}
}
*/
// func, date, undef, and sym are lost

Functions and undefined simply disappear, Date objects turn into strings, and symbols are omitted entirely. This means that JSON cloning is only suitable for plain data objects composed of primitives, arrays, and other plain objects.

Another limitation is that JSON cloning cannot handle circular references. If your object contains a cycle, JSON.stringify() will throw a TypeError. For example:

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

try {
  JSON.parse(JSON.stringify(circular));
} catch (e) {
  console.error(e); // TypeError: Converting circular structure to JSON
}

To work around this, you’d need to either remove circular references before cloning or use libraries that support cycles.

Despite these limitations, the JSON method remains a fast and easy shortcut for deep cloning simple data structures, especially when you know the shape of your data and are confident it fits within JSON’s constraints.

Here’s a reusable function wrapping this technique:

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

Use it when you want a quick deep clone of plain objects or arrays, but avoid it when your data contains functions, dates, special objects, or circular references. For those cases, more robust solutions are necessary, which we’ll explore next.

Exploring libraries for advanced deep cloning solutions

When you need a reliable deep clone beyond the limitations of JSON methods, turning to well-established libraries is the pragmatic choice. These libraries handle edge cases like circular references, special object types, and preserve prototypes and functions where applicable.

lodash is the classic go-to utility library that includes a _.cloneDeep() method. It covers most cloning needs with reasonable performance and a simpler API:

import _ from "lodash";

const original = {
  name: "Joel",
  preferences: {
    theme: "dark",
    notifications: true
  },
  created: new Date(),
  greet() {
    console.log("Hi!");
  }
};

const clone = _.cloneDeep(original);
clone.preferences.theme = "light";
clone.greet();

console.log(original.preferences.theme); // "dark"

Unlike JSON cloning, _.cloneDeep() preserves Date objects, functions, and circular references. It also handles arrays, Maps, Sets, and other built-in types fairly well. However, it doesn’t clone custom class instances perfectly if they have methods relying on internal state or non-enumerable properties.

If you want a smaller, dedicated deep cloning package, rfdc (Really Fast Deep Clone) is a minimalist alternative. It’s extremely fast because it uses recursion without any fancy type detection, but it does not support cloning functions or handle circular references out of the box:

import rfdc from "rfdc";

const clone = rfdc()(original);
clone.preferences.theme = "light";

console.log(original.preferences.theme); // "dark"

Use rfdc when performance matters and your data is mostly plain objects and arrays without cycles or functions.

For full control, clone-deep is another popular library that supports cloning complex structures and circular references, with options to customize cloning behavior:

import cloneDeep from "clone-deep";

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

const clonedCircular = cloneDeep(circular);
console.log(clonedCircular.self === clonedCircular); // true

This library is useful when dealing with graphs or complex nested data where cycles are common.

For even more advanced use cases, especially if you need to clone class instances while preserving prototypes and non-enumerable properties, you might need to write a custom cloning function or use serialization libraries like serialijse or flatted. These libraries can serialize and deserialize objects with cycles and class information, but they come with added complexity and dependencies.

Here’s a quick example using flatted to handle circular references:

import { parse, stringify } from "flatted";

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

const serialized = stringify(circular);
const deserialized = parse(serialized);

console.log(deserialized.self === deserialized); // true

While flatted doesn’t clone functions or class prototypes, it solves the circular reference problem elegantly and can be combined with other cloning strategies.

In summary, the choice of deep cloning library depends heavily on what your object contains and what guarantees you need:

  • lodash for general purpose, robust cloning including functions and dates.
  • rfdc for blazing fast cloning of plain objects and arrays.
  • clone-deep for complex nested data with circular references.
  • flatted or serialijse for serializing objects with cycles and class metadata.

Each has trade-offs in size, speed, and fidelity, so choose wisely depending on your project’s requirements.

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 *