Were you aware that in JavaScript, you can easily create clones of deeply nested objects without relying on external libraries like lodash? This can be achieved using a single built-in function called structuredClone(), which is part of the Web APIs.

Take a look at the example below, where we have a currentUser object with nested objects, a Date object, and an array:

const currentUser = {
  full_name: {
    first_name: 'Anil',
    last_name: 'Seervi'
  },
  joined_at: new Date(0),
  languages: ['English', 'JavaScript']
};

const cloneUser = structuredClone(currentUser);

As you can see, the structuredClone() function is able to create a clone of currentUser while preserving the nested full_name object, the Date object joined_at, and the array languages, all in the cloneUser variable.

currentUser.full_name; // Object: {first_name: "Anil", last_name: "Seervi"}
currentUser.joined_at; // Date: Thu Jan 01 1970 05:30:00 GMT+0530 (India Standard Time)
currentUser.languages; // Array: ["English", "JavaScript"]

In addition to preserving these types of objects, structuredClone() is capable of cloning other types of objects as well. We will discuss these types and any restrictions associated with structuredClone() in the following sections. But first, let's take a closer look at the syntax of the structuredClone() function.

Syntax

structuredClone(value);
structuredClone(value, options);

Parameters

value : The object to be cloned. This can be any structured-cloneable type.
options(optional) :
An object with the following properties:
transfer : An array of transferable objects that will be moved rather than cloned to the returned object.

Return Type

The returned value is a deep copy of the original value.

Exceptions

DataCloneError DOMException

Thrown if any part of the input value is not serializable.

Structured cloneable types

The structuredClone() function is capable of cloning not only primitive types, but also more complex types in JavaScript. For example, it can clone objects with infinite nested objects and arrays, circular references, and various JavaScript types including Date, Set, Map, Error, RegExp, ArrayBuffer, Blob, File, ImageData, and many others, as listed in the Mozilla documentation.

Consider the multipleTypesObject example below:

const multipleTypesObject = {
  set: new Set([1, 3, 2]),
  map: new Map([[3, 2]]),
  regex: /foobar/,
  deep: { array: [{ file: new File(someBlobData, 'file.txt') }] },
  error: new Error('Hello!')
};
multipleTypesObject.circular = multipleTypesObject;

const fullyCloned = structuredClone(multipleTypesObject);
// ✅ All good, fully and deeply copied!

In this example, multipleTypesObject contains various types of objects, such as a Set, Map, RegExp, nested objects with File, and an Error object. It also includes a circular reference where multipleTypesObject refers to itself. Despite these complexities, structuredClone() is able to create a fully cloned copy in the fullyCloned variable, including all nested objects and circular references, without losing any data or encountering errors.

Preserving circular references

The structuredClone() function can also be used to perform a deep copy of an object while preserving any circular references within that object. In the example below, the user object is created with a circular reference to itself using the property itself:

// Create an object with a value and a circular reference to itself.
const user = { name: 'Anil' };
user.itself = user;

By using structuredClone() to clone the user object, the circular reference is preserved in the clone object, as demonstrated in the following assertions:

// Clone it
const clone = structuredClone(user);

console.assert(clone !== user); // the objects are not the same (not same identity)
console.assert(clone.name === 'Anil'); // they do have the same values
console.assert(clone.itself === clone); // and the circular reference is preserved

The first assertion confirms that the clone and user objects are not the same and do not share the same identity. The second assertion verifies that the cloned object clone has the same values as the original object user. Finally, the third assertion confirms that the circular reference itself in the clone object still points to itself, preserving the circular reference in the cloned object as well.

Transferring values

In addition to deep cloning objects, the structuredClone() function also allows you to transfer certain Transferable objects from the original object to the cloned object, using the transfer property of the options parameter. This transfer operation makes the original object unusable, as the transferred data is removed from the original object.

In the example below, a uInt8Array is created with a byte length of 16MB:

// 16MB = 1024 * 1024 * 16
const uInt8Array = Uint8Array.from({ length: 1024 * 1024 * 16 }, (v, i) => i);

console.log(uInt8Array.byteLength); // 16777216

By passing the uInt8Array and specifying [uInt8Array.buffer] as the value for the transfer property in the options parameter of structuredClone(), the data in uInt8Array is transferred to the cloned object transferred:

const transferred = structuredClone(uInt8Array, {
  transfer: [uInt8Array.buffer]
});
console.log(uInt8Array.byteLength); // 0, because it was buffer was transferred

After the transfer, the uInt8Array object becomes unusable, as its byteLength is set to 0, indicating that the data has been successfully transferred to the cloned object transferred.

Why not spread the objects?

It is important to note that spreading an object using the spread syntax (...) in JavaScript will only create a shallow copy of the object, and not a deep copy. This means that nested objects within the original object will still share the same references in the copied object, and updating one will also update the other. This behavior can lead to unexpected results when working with complex objects that have nested objects or arrays.

Let's take a closer look at the example below:

const currentUser = {
  full_name: {
    first_name: 'Anil',
    last_name: 'Seervi'
  },
  joined_at: new Date(0),
  languages: ['English', 'JavaScript']
};

const spreadObject = {
  ...currentUser,
  full_name: { ...currentUser.full_name }
};

In this example, currentUser is an object with three properties: full_name, joined_at, and languages. The full_name property is an object with two nested properties: first_name and last_name, and the languages property is an array.

Using the spread syntax, we create a shallow copy of currentUser and store it in spreadObject. However, the nested full_name object in spreadObject is still a shallow copy of the full_name object in currentUser. This means that both full_name objects still share the same references, and updating one will also update the other. Similarly, the languages array in spreadObject is still a reference to the same array in currentUser.

As a result, the following operations will have unintended consequences:

// 🚩 oops - we just added "CSS" to both the copy *and* the original array
spreadObject.languages.push('CSS');

// 🚩 oops - we just updated the date for the copy *and* original date
spreadObject.joined_at.setTime(969);

Both the languages array in spreadObject and the joined_at date object in spreadObject will be updated, but the same changes will also be reflected in the currentUser object, as they are still sharing the same references.

JSON.parse(JSON.stringify(x)) to the rescue?

Using JSON.parse(JSON.stringify(x)) as a way to create a copy of an object in JavaScript may seem convenient and fast, but it has several shortcomings:

  1. Loss of type information: When using JSON.stringify followed by JSON.parse to clone an object, any non-string object types such as Date, Set, Map, RegExp, File, and Error will be converted to string representations or empty objects in the resulting copy. This means that the copied object will lose its original data types, as demonstrated in the example:
const event = {
  title: 'Pusblish new article',
  date: new Date('4/1/2023')
};

// 🚩 JSON.stringify converted the `date` to a string
const wrongEvent = JSON.parse(JSON.stringify(event));

console.log(wrongEvent);
/*
{
  title: "Publish new article",
  date: "2023-03-31T18:30:00.000Z"
}
*/

In this example, the date property, which was originally a Date object, is converted to a string in the copied object, losing its original data type.

  1. Inability to clone certain object types: JSON.stringify and JSON.parse cannot properly clone objects of certain types, such as Set, Map, RegExp, File, and Error, as demonstrated in the example:
const multipleTypesObject = {
  set: new Set([1, 3, 2]),
  map: new Map([[3, 2]]),
  regex: /foobar/,
  deep: { array: [{ file: new File(someBlobData, 'file.txt') }] },
  error: new Error('Hello!')
};

const totallyWrongCopy = JSON.parse(JSON.stringify(multipleTypesObject));

If we try logging totallyWrongCopy we would get :

{
  "set": {},
  "map": {},
  "regex": {},
  "deep": {
    "array": [
      { file:{}}
    ]
  },
  "error": {},
}

In this example, the set, map, regex, and error properties are not properly cloned and are instead converted to empty objects or empty arrays in the copied object, losing their original data and behavior.

  1. Inability to clone circular objects: JSON.stringify cannot handle circular references in objects, as it will result in an error. Circular references occur when an object references itself, either directly or indirectly through a chain of references. This limitation makes JSON.stringify unsuitable for cloning objects that contain circular references.

In summary, while JSON.parse(JSON.stringify(x)) may be a convenient and fast way to create a copy of simple objects in JavaScript, it has limitations in preserving original data types, cloning certain object types, and handling circular references.

What can structuredClone not clone ?

The structuredClone function in JavaScript has some limitations on what it can clone:

  1. Function objects: Function objects cannot be duplicated by the structured clone algorithm, and attempting to clone a Function object will throw a DataCloneError exception.

Example:

// Throws DataCloneError
structuredClone({ fn: () => {} });
  1. DOM nodes: Cloning DOM nodes using structuredClone will also throw a DataCloneError exception.

Example:

// Throws DataCloneError
structuredClone({ el: document.body });
  1. Certain object properties: Some object properties are not preserved during cloning. For example, the lastIndex property of RegExp objects is not preserved. Property descriptors, setters, getters, and similar metadata-like features are not duplicated either. For example, if an object has a property descriptor that marks it as readonly, the cloned object will be read/write, as that's the default behavior.

Example:

structuredClone({
  get foo() {
    return 'bar';
  }
});
// Becomes: { foo: 'bar' }
  1. Object prototypes: The prototype chain is not walked or duplicated during cloning. This means that the cloned object will not inherit the same prototype chain as the original object. As a result, instanceof checks may return false for the cloned object, even if it was cloned from an instance of the same class.

Example:

// 🚩 Object prototypes
class MyClass {
  foo = 'bar';
  myMethod() {
    /* ... */
  }
}
const myClass = new MyClass();

const cloned = structuredClone(myClass);
// Becomes: { foo: 'bar' }

cloned instanceof myClass; // false

It's important to keep these limitations in mind when using the structuredClone function, and choose the appropriate cloning technique based on the specific requirements of your use case.

Full list of supported types

More simply put, anything not in the below list cannot be cloned:

JS Built-ins:

Error types:

Web/API types:

It's important to note that only plain objects (e.g., object literals) of the Object type are supported for cloning, and not all objects of Object type. Additionally, only specific error types and Web/API types listed above are supported for cloning. Anything not in the above list cannot be cloned using the structuredClone function. It's important to consider these limitations when using the structuredClone function and choose an appropriate cloning technique for objects not supported by it.

Browsers and Runtime support for structuredClone

Apart from the availability in workers, structuredClone has pretty good support in all major browsers and runtimes.

Browser Support for structuredClone