Native Deep Cloning: Going Beyond Lodash in JavaScript
Last updated
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:
- Loss of type information: When using
JSON.stringify
followed byJSON.parse
to clone an object, any non-string object types such asDate
,Set
,Map
,RegExp
,File
, andError
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.
- Inability to clone certain object types:
JSON.stringify
andJSON.parse
cannot properly clone objects of certain types, such asSet
,Map
,RegExp
,File
, andError
, 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.
- 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 makesJSON.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:
Function
objects:Function
objects cannot be duplicated by the structured clone algorithm, and attempting to clone aFunction
object will throw aDataCloneError
exception.
Example:
// Throws DataCloneError
structuredClone({ fn: () => {} });
- DOM nodes: Cloning DOM nodes using
structuredClone
will also throw aDataCloneError
exception.
Example:
// Throws DataCloneError
structuredClone({ el: document.body });
- Certain object properties: Some object properties are not preserved during cloning. For example, the
lastIndex
property ofRegExp
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' }
- 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 returnfalse
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:
Array
ArrayBuffer
Boolean
DataView
Date
Map
- Plain objects (e.g., object literals) of
Object
type - Primitive types, except
symbol
(i.e.,number
,string
,null
,undefined
,boolean
,BigInt
) RegExp
Set
- TypedArray
Error types:
Web/API types:
AudioData
Blob
CryptoKey
DOMException
DOMMatrix
DOMMatrixReadOnly
DOMPoint
DomQuad
DomRect
File
FileList
FileSystemDirectoryHandle
FileSystemFileHandle
FileSystemHandle
ImageBitmap
ImageData
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.
Anil Seervi
Hi I'm a Front-end developer with extensive experience in creating beautiful and user-friendly interfaces that bring websites to life.