Deep copy

A deep copy of an object is a copy whose properties do not share the same references (point to the same underlying values) as those of the source object from which the copy was made. As a result, when you change either the source or the copy, you can be assured you're not causing the other object to change too. That behavior contrasts with the behavior of a shallow copy, in which changes to nested properties in the source or the copy may cause the other object to change too.

Deep copying is usually implemented recursively as such:

  1. A new object of the same type is created. The prototype chain may or may not be copied (and usually it is not), but for example deep copying an Array should result in an Array and not something else.
  2. For each own property of the original object, a property with the same key and descriptors is defined on the new object.
  3. Each new property's value is set to a deep copy of the original property's value. If the property's value is a primitive, no copying is done.
  4. Any data that's not exposed as properties (such as Map) is deeply copied, provided that the implementation recognizes the object type and knows how to retrieve and set the data.
  5. Usually there's some support for circular references.

Note that because JavaScript has no built-in mechanism that performs a true deep copy, library implementations often diverge on technical details, such as:

  • Whether non-enumerable or symbol properties are copied
  • Whether accessor properties are copied as accessors
  • Whether prototype properties are copied
  • What data structures support copying non-property data

The copy of an object whose properties all have primitive values fits the definition of both a deep copy and a shallow copy. It is somewhat useless to talk about the depth of such a copy, though, because it has no nested properties and we usually talk about deep copying in the context of mutating nested properties.

In JavaScript, standard built-in object-copy operations (spread syntax, Array.prototype.concat(), Array.prototype.slice(), Array.from(), and Object.assign()) do not create deep copies (instead, they create shallow copies).

One way to make a deep copy of a JavaScript object, if it can be serialized, is to use JSON.stringify() to convert the object to a JSON string, and then JSON.parse() to convert the string back into a (completely new) JavaScript object:

js
const ingredientsList = ["noodles", { list: ["eggs", "flour", "water"] }];
const ingredientsListDeepCopy = JSON.parse(JSON.stringify(ingredientsList));

Because a deep copy shares no references with its source object, any changes made to the deep copy do not affect the source object.

js
// Change the value of the 'list' property in ingredientsListDeepCopy.
ingredientsListDeepCopy[1].list = ["rice flour", "water"];
// The 'list' property does not change in ingredients_list.
console.log(ingredientsList[1].list);
// Array(3) [ "eggs", "flour", "water" ]

However, while the object in the code above is simple enough to be serializable, many JavaScript objects are not serializable at all — for example, functions (with closures), Symbols, objects that represent HTML elements in the HTML DOM API, recursive data, and many other cases. Calling JSON.stringify() to serialize the objects will fail in such cases. Deep-copying these objects requires other APIs or libraries.

The web API structuredClone() also creates deep copies and has the advantage of allowing transferable objects in the source to be transferred to the new copy, rather than just cloned. It also handles more data types, such as Error. But note that structuredClone() isn't a feature of the JavaScript language itself — instead it's a feature of browsers and other JavaScript hosts that implement web APIs. And calling structuredClone() to clone a non-serializable object will fail in the same way that calling JSON.stringify() to serialize it will fail.