The Platform now ships with structuredClone(), a built-in function for deep-copying.
For the longest time, you had to resort to workarounds and libraries to create a deep copy of a JavaScript value. The Platform now ships with structuredClone()
, a built-in function for deep-copying.
Shallow copies
Copying a value in JavaScript is almost always shallow, as opposed to deep. That means that changes to deeply nested values will be visible in the copy as well as the original.
One way to create a shallow copy in JavaScript using the object spread operator ...
:
const myOriginal = {
someProp: "with a string value",
anotherProp: {
withAnotherProp: 1,
andAnotherProp: true
}
};
const myShallowCopy = {...myOriginal};
Adding or changing a property directly on the shallow copy will only affect the copy, not the original:
myShallowCopy.aNewProp = "a new value";
console.log(myOriginal.aNewProp)
// ^ logs `undefined`
However, adding or changing a deeply nested property affects both the copy and the original:
myShallowCopy.anotherProp.aNewProp = "a new value";
console.log(myOriginal.anotherProp.aNewProp)
// ^ logs `a new value`
The expression {...myOriginal}
iterates over the (enumerable) properties of myOriginal
using the Spread Operator. It uses the property name and value, and assigns them one by one to a freshly created, empty object. As such, the resulting object is identical in shape, but with its own copy of the list of properties and values. The values are copied, too, but so-called primitive values are handled differently by the JavaScript value than non-primitive values. To quote MDN:
In JavaScript, a primitive (primitive value, primitive data type) is data that is not an object and has no methods. There are seven primitive data types: string, number, bigint, boolean, undefined, symbol, and null.
MDN — Primitive
Non-primitive values are handled as references, meaning that the act of copying the value is really just copying a reference to the same underlying object, resulting in the shallow copy behavior.
Deep copies
The opposite of a shallow copy is a deep copy. A deep copy algorithm also copies an object’s properties one by one, but invokes itself recursively when it finds a reference to another object, creating a copy of that object as well. This can be very important to make sure that two pieces of code don’t accidentally share an object and unknowingly manipulate each others’ state.
There used to be no easy or nice way to create a deep-copy of a value in JavaScript. Many people relied on third-party libraries like Lodash’s cloneDeep()
function. Arguably the most common solution to this problem was a JSON-based hack:
const myDeepCopy = JSON.parse(JSON.stringify(myOriginal));
In fact, this was such a popular workaround, that V8 aggressively optimized JSON.parse()
and specifically the pattern above to make it as fast as possible. And while it is fast, it comes with a couple of shortcomings and tripwires:
- Recursive data structures:
JSON.stringify()
will throw when you give it a recursive data structure. This can happen quite easily when working with linked lists or trees. - Built-in types:
JSON.stringify()
will throw if the value contains other JS built-ins likeMap
,Set
,Date
,RegExp
orArrayBuffer
. - Functions:
JSON.stringify()
will quietly discard functions.
Structured cloning
The platform already needed the ability to create deep copies of JavaScript values in a couple of places: Storing a JS value in IndexedDB requires some form of serialization so it can be stored on disk and later deserialized to restore the JS value. Similarly, sending messages to a WebWorker via postMessage()
requires transferring a JS value from one JS realm to another. The algorithm that is used for this is called “Structured Clone”, and until recently, wasn’t easily accessible to developers.
That has now changed! The HTML spec was amended to expose a function called structuredClone()
that runs exactly that algorithm as a means for developers to easily create deep copies of JavaScript values.
const myDeepCopy = structuredClone(myOriginal);
That’s it! That’s the entire API. If you want to dive deeper into the details, take a look at the MDN article.
Features and limitations
Structured cloning addresses many (although not all) shortcomings of the JSON.stringify()
technique. Structured cloning can handle cyclical data structures, support many built-in data types and is generally more robust and often faster.
However, it still has some limitations that may catch you off-guard:
- Prototypes: If you use
structuredClone()
with a class instance, you’ll get a plain object as the return value, as structured cloning discards the object’s prototype chain. - Functions: If your object contains functions,
structuredClone()
will throw aDataCloneError
exception. - Non-cloneables: Some values are not structured cloneable, most notably
Error
and DOM nodes. It will causestructuredClone()
to throw.
If any of these limitations are a deal-breaker for your use-case, libraries like Lodash still provide custom implementations of other deep-cloning algorithms that may or may not fit your use-case.
Performance
While I haven’t done a new micro-benchmark comparison, I did a comparison in early 2018, before structuredClone()
was exposed. Back then, JSON.parse()
was the fastest option for very small objects. I expect that to remain the same. Techniques that relied on structured cloning were (significantly) faster for bigger objects. Considering that the new structuredClone()
comes without the overhead of abusing other APIs and is more robust than JSON.parse()
, I recommend you make it your default approach for creating deep copies.
Conclusion
If you need to create a deep-copy of a value in JS—maybe that be because you use immutable data structures or you want to make sure a function can manipulate an object without affecting the original—you no longer need to reach for workarounds or libraries. The JS ecosystem now has structuredClone()
. Huzzah.