structuredClone()
: The Modern SolutionstructuredClone()
structuredClone()
structuredClone()
In JavaScript, copying objects is a common task, but it's not always straightforward. Understanding the difference between shallow and deep cloning is crucial to avoid unintended side effects and bugs in your applications. This article dives into deep cloning, explores traditional methods, and highlights the modern, built-in structuredClone()
method.
When you copy an object in JavaScript, you might be creating either a shallow copy or a deep copy.
Shallow Copy: Only the top-level properties of an object are copied. If a property holds a reference to another object (like an array or another object), the copy will point to the same referenced object. Modifying the nested object in the copy will also affect the original, and vice-versa.
javascript:
const original = { name: "Alice", details: { age: 30 } }; // Shallow copy using spread syntax const shallowCopy = { ...original }; shallowCopy.details.age = 31; console.log(original.details.age); // Output: 31 (original is affected!)
Deep Copy: All properties of the object are copied recursively. This means that if a property is an object, that object is also deeply copied. The new copy is completely independent of the original.
javascript:
// (Conceptual example, actual deep clone method will be shown later) const original = { name: "Bob", details: { age: 25 } }; const deepCopy = someDeepCloneFunction(original); deepCopy.details.age = 26; console.log(original.details.age); // Output: 25 (original remains unchanged)
Deep cloning is essential when you need to manipulate a copy of an object without affecting the source, especially in state management, data manipulation, or when working with history (undo/redo functionalities).
Before structuredClone()
, developers relied on various techniques for deep cloning, each with its own set of limitations.
JSON.parse(JSON.stringify(object))
This is a common and quick way to deep clone objects that are "JSON-safe" (i.e., can be serialized to a JSON string and then parsed back).
javascript:
const original = { name: "Charlie", hobbies: ["reading", "hiking"], joined: new Date("2023-01-15") }; const cloned = JSON.parse(JSON.stringify(original)); console.log(cloned.name); // "Charlie" console.log(cloned.hobbies); // ["reading", "hiking"] console.log(typeof cloned.joined); // "string" (Date object became a string!) cloned.hobbies.push("cycling"); console.log(original.hobbies); // ["reading", "hiking"] (hobbies array is deeply cloned)
Pros:
Cons:
undefined
values, Symbol
s, RegExp
objects, Map
s, Set
s are either lost or incorrectly converted. For example, Date
objects are converted to ISO date strings.Developers often wrote custom recursive functions to traverse and clone objects property by property. Libraries like Lodash (_.cloneDeep()
) also provide robust deep cloning solutions.
Pros:
Cons:
structuredClone()
: The Modern SolutionJavaScript now has a built-in global function, structuredClone()
, designed for high-fidelity deep cloning. It uses the structured clone algorithm, which was originally developed for copying JavaScript values between Web Workers and the main thread.
javascript:
const original = { name: "David", birthDate: new Date("1990-05-20"), regex: /pattern/gi, data: new Map([["key1", "value1"], ["key2", "value2"]]), set: new Set([1, 2, 3]), typedArr: new Uint8Array([10, 20]), nested: { value: 42 } }; // Add a circular reference original.self = original; try { const cloned = structuredClone(original); console.log(cloned.name); // "David" console.log(cloned.birthDate); // Date object (not a string!) console.log(cloned.birthDate instanceof Date); // true console.log(cloned.regex); // /pattern/gi console.log(cloned.data.get("key1")); // "value1" console.log(cloned.set.has(2)); // true console.log(cloned.typedArr[0]); // 10 // Modify the clone cloned.name = "David Clone"; cloned.birthDate.setFullYear(1991); cloned.data.set("key1", "newValue"); cloned.nested.value = 100; console.log(original.name); // "David" (original is unchanged) console.log(original.birthDate.getFullYear()); // 1990 (original is unchanged) console.log(original.data.get("key1")); // "value1" (original is unchanged) console.log(original.nested.value); // 42 (original is unchanged) // Check the circular reference console.log(cloned.self === cloned); // true (circular reference is preserved) console.log(cloned.self === original); // false (it's a new circular reference within the clone) } catch (error) { console.error("Cloning failed:", error); }
structuredClone()
:JSON.parse(JSON.stringify())
supports, including:ArrayBuffer
BigInt64Array
, BigUint64Array
Blob
Boolean
CryptoKey
DataView
Date
Error
types (some properties might be lost, like fileName
, lineNumber
, columnNumber
)File
, FileList
Float32Array
, Float64Array
ImageBitmap
ImageData
Int8Array
, Int16Array
, Int32Array
Map
Number
Object
(plain objects)RegExp
(though the lastIndex
property is not preserved)Set
String
Uint8Array
, Uint8ClampedArray
, Uint16Array
, Uint32Array
ArrayBuffer
, MessagePort
, or ImageBitmap
, structuredClone()
can transfer ownership of the underlying data to the new object if specified via an options argument (e.g., structuredClone(value, { transfer: [arrayBuffer] })
). This can be very efficient as it avoids copying large data.JSON.stringify()
, structuredClone()
can correctly duplicate objects with circular references.JSON.parse(JSON.stringify())
and often competitive with library solutions for common use cases.structuredClone()
While powerful, structuredClone()
is not a silver bullet and has limitations. According to MDN's documentation on the structured clone algorithm and the structuredClone()
page, here are key things it cannot clone:
Functions: Attempting to clone an object containing functions will result in a DataCloneError
. Functions are not serializable by this algorithm.
javascript:
const objWithFunc = { name: "Test", greet: function() { console.log("Hello!"); } }; try { const cloned = structuredClone(objWithFunc); } catch (e) { console.error(e.name, e.message); // "DataCloneError", "() => { console.log("Hello!"); } could not be cloned." }
DOM Nodes: DOM elements cannot be cloned using structuredClone()
. This will also throw a DataCloneError
. If you need to clone DOM nodes, use methods like node.cloneNode(true)
.
javascript:
const myDiv = document.createElement("div"); myDiv.textContent = "Hello"; try { const clonedDiv = structuredClone(myDiv); } catch (e) { console.error(e.name, e.message); // "DataCloneError", "#<HTMLDivElement> could not be cloned." (Exact message may vary by browser) }
Property Descriptors, Setters, and Getters: The algorithm clones the values of properties. It does not preserve property descriptors (e.g., writable
, configurable
, enumerable
), nor does it duplicate getters and setters. The cloned object will have plain properties with the values obtained from the original's getters.
javascript:
const originalWithAccessor = { _privateValue: 1, get value() { return this._privateValue * 2; }, set value(val) { this._privateValue = val; } }; Object.defineProperty(originalWithAccessor, 'readOnlyProp', { value: "constant", writable: false }); console.log(originalWithAccessor.value); // 2 const clonedAccessor = structuredClone(originalWithAccessor); console.log(clonedAccessor.value); // 2 (the result of the getter is cloned as a data property) console.log(Object.getOwnPropertyDescriptor(clonedAccessor, 'value')); // { value: 2, writable: true, enumerable: true, configurable: true } // The getter/setter is gone, 'value' is now a simple data property. clonedAccessor.value = 10; // This changes clonedAccessor._privateValue (if it were cloned) or clonedAccessor.value directly console.log(clonedAccessor.value); // 10 console.log(originalWithAccessor.value); // 2 (original is unaffected, which is good) const desc = Object.getOwnPropertyDescriptor(clonedAccessor, 'readOnlyProp'); console.log(desc.writable); // true - was false on original
Object Prototypes: The prototype chain is not traversed or duplicated. The cloned object will always be a plain object or an instance of a built-in type (like Date
, Map
, etc.). If you clone an instance of a custom class, the clone will be a plain object, and it will not be an instanceof
your custom class, nor will it inherit methods from the class prototype.
javascript:
class MyClass { constructor(name) { this.name = name; } greet() { return `Hello, ${this.name}`; } } const myInstance = new MyClass("Eve"); console.log(myInstance.greet()); // "Hello, Eve" console.log(myInstance instanceof MyClass); // true const clonedInstance = structuredClone(myInstance); console.log(clonedInstance.name); // "Eve" console.log(clonedInstance instanceof MyClass); // false // console.log(clonedInstance.greet()); // TypeError: clonedInstance.greet is not a function
Other Non-Serializable Objects: Certain built-in types like WeakMap
, WeakSet
, and some platform-specific objects might not be cloneable. The exact list can be nuanced and environment-dependent. Always refer to the latest MDN documentation.
structuredClone()
structuredClone()
is an excellent choice for many deep cloning scenarios:
Transferable
objects explicitly).JSON.parse(JSON.stringify())
.However, if you need to:
...then you'll still need to resort to custom cloning functions or established libraries like Lodash's _.cloneDeep()
.
The structuredClone()
method is a significant addition to JavaScript, providing a robust, built-in mechanism for deep cloning a wide array of data types. It addresses many of the shortcomings of older techniques like JSON.parse(JSON.stringify())
, especially regarding type fidelity and handling circular references.
While it has limitations, particularly with functions and DOM nodes, structuredClone()
simplifies many common deep cloning tasks, leading to cleaner, more reliable code. Understanding its capabilities and limitations, as outlined on MDN (Window.structuredClone() and Structured clone algorithm), is key to leveraging it effectively in your JavaScript projects.