JavaScript Structured Clone: Deep Copying Objects
JavaScript developers constantly wrestle with copying objects. The language's reference-based nature means that simple assignments don't create copies—they create new references to the same data....
Key Insights
structuredClone()is a native JavaScript API that creates true deep copies of objects, handling nested structures, circular references, and complex types like Maps and Sets that JSON methods can’t handle- While powerful, it can’t clone functions, DOM nodes, or prototype chains—understanding these limitations prevents runtime errors in production code
- For most deep cloning needs,
structuredClone()outperforms alternatives likeJSON.parse(JSON.stringify())in both capability and developer experience, though JSON methods remain faster for simple, serializable objects
The Deep Copy Problem
JavaScript developers constantly wrestle with copying objects. The language’s reference-based nature means that simple assignments don’t create copies—they create new references to the same data. This becomes particularly problematic with nested objects.
The spread operator and Object.assign() only perform shallow copies. They copy top-level properties, but nested objects remain references to the original data:
const original = {
name: 'John',
address: {
city: 'New York',
zip: '10001'
}
};
const shallowCopy = { ...original };
shallowCopy.address.city = 'Boston';
console.log(original.address.city); // 'Boston' - original mutated!
This behavior causes bugs that are difficult to track down. You modify what you think is a copy, only to discover you’ve corrupted the original data. State management libraries, form handlers, and any code dealing with complex data structures need reliable deep copying.
For years, developers resorted to workarounds: JSON.parse(JSON.stringify()), third-party libraries like Lodash, or writing custom recursive functions. Each approach had drawbacks. Now we have structuredClone().
What is the Structured Clone Algorithm?
The structured clone algorithm has existed in browsers since the early 2010s, powering APIs like postMessage() for Web Workers and IndexedDB storage. It’s the mechanism browsers use to serialize data when sending it between different execution contexts.
In 2022, structuredClone() became available as a global function, exposing this algorithm directly to developers. It’s now supported in all modern browsers (Chrome 98+, Firefox 94+, Safari 15.4+) and Node.js 17+.
The function is straightforward to use:
const original = {
name: 'John',
address: {
city: 'New York',
coordinates: {
lat: 40.7128,
lng: -74.0060
}
},
tags: ['developer', 'writer']
};
const deepCopy = structuredClone(original);
deepCopy.address.coordinates.lat = 41.8781;
console.log(original.address.coordinates.lat); // 40.7128 - unchanged!
console.log(deepCopy.address.coordinates.lat); // 41.8781
Unlike shallow copy methods, modifications to nested properties don’t affect the original. The entire object tree is duplicated.
What Can (and Can’t) Be Cloned
The structured clone algorithm supports a wide range of JavaScript types beyond plain objects and arrays. It handles Maps, Sets, Dates, RegExp, typed arrays, ArrayBuffers, and more:
const complex = {
date: new Date('2024-01-01'),
regex: /[a-z]+/gi,
map: new Map([['key1', 'value1'], ['key2', 'value2']]),
set: new Set([1, 2, 3]),
buffer: new Uint8Array([1, 2, 3, 4]),
nested: {
deepMap: new Map([['nested', new Set([4, 5, 6])]])
}
};
const cloned = structuredClone(complex);
// All types properly cloned
console.log(cloned.date instanceof Date); // true
console.log(cloned.map.get('key1')); // 'value1'
console.log(cloned.set.has(2)); // true
However, certain types cannot be cloned. Functions, DOM nodes, symbols (as property keys), and prototype chains are explicitly not supported:
const withFunction = {
data: 'test',
method: function() { return this.data; }
};
try {
structuredClone(withFunction);
} catch (error) {
console.error(error); // DataCloneError: function could not be cloned
}
This limitation is by design. Functions have closures and scope chains that can’t be meaningfully serialized. DOM nodes are browser-specific objects that don’t exist in other contexts. When you need to clone objects with methods, you’ll need to reconstitute them after cloning or use a different approach.
Class instances lose their prototype chain:
class Person {
constructor(name) {
this.name = name;
}
greet() {
return `Hello, ${this.name}`;
}
}
const person = new Person('Alice');
const cloned = structuredClone(person);
console.log(cloned.name); // 'Alice'
console.log(cloned instanceof Person); // false
console.log(cloned.greet); // undefined
The data is cloned, but the object becomes a plain object without its class methods.
Handling Circular References
One of structuredClone()’s most powerful features is automatic handling of circular references. Objects that reference themselves or create reference cycles clone without errors:
const circular = {
name: 'Node 1',
data: { value: 42 }
};
// Create circular reference
circular.self = circular;
circular.data.parent = circular;
const cloned = structuredClone(circular);
console.log(cloned.self === cloned); // true - circular reference preserved
console.log(cloned.data.parent === cloned); // true
console.log(cloned !== circular); // true - different object
The algorithm tracks objects it has already visited and reuses the cloned references when it encounters them again. This maintains the structural relationships in the cloned object.
Compare this to JSON.parse(JSON.stringify()), which throws an error on circular references:
const circular = { name: 'test' };
circular.self = circular;
try {
JSON.parse(JSON.stringify(circular));
} catch (error) {
console.error(error); // TypeError: Converting circular structure to JSON
}
This makes structuredClone() essential for cloning graph-like data structures, linked lists, or any complex object model where entities reference each other.
Performance Considerations
Performance varies depending on your use case. For simple objects containing only primitives and plain nested objects, JSON.parse(JSON.stringify()) is typically faster:
const simpleData = {
users: Array.from({ length: 1000 }, (_, i) => ({
id: i,
name: `User ${i}`,
email: `user${i}@example.com`
}))
};
console.time('JSON method');
const jsonClone = JSON.parse(JSON.stringify(simpleData));
console.timeEnd('JSON method'); // ~2-3ms
console.time('structuredClone');
const structuredClone = structuredClone(simpleData);
console.timeEnd('structuredClone'); // ~4-6ms
However, structuredClone() pulls ahead when dealing with types that JSON can’t handle. You avoid the overhead of converting Dates to strings and back, or losing Map and Set structures entirely.
More importantly, structuredClone() is more correct. It handles edge cases that JSON methods fail on:
- Dates remain Date objects instead of becoming strings
undefinedvalues are preserved (JSON.stringify removes them)NaNandInfinityare preserved (JSON converts them tonull)- Circular references work instead of throwing errors
For most applications, the performance difference is negligible compared to the correctness benefits. Use structuredClone() as your default, and only optimize to JSON methods if profiling shows it’s a bottleneck.
Practical Use Cases
State Management: When implementing immutable state updates, structuredClone() ensures you never accidentally mutate the original state:
function updateUserPreferences(state, userId, newPreferences) {
const newState = structuredClone(state);
const user = newState.users.find(u => u.id === userId);
if (user) {
user.preferences = { ...user.preferences, ...newPreferences };
}
return newState;
}
const state = {
users: [
{ id: 1, name: 'Alice', preferences: { theme: 'dark', notifications: true } }
]
};
const updated = updateUserPreferences(state, 1, { theme: 'light' });
console.log(state.users[0].preferences.theme); // 'dark' - unchanged
Form Data Manipulation: Clone form data before validation or transformation, preserving the original for reset functionality:
function validateAndTransformFormData(formData) {
const workingCopy = structuredClone(formData);
// Transform without affecting original
workingCopy.email = workingCopy.email.toLowerCase().trim();
workingCopy.submittedAt = new Date();
return workingCopy;
}
Undo/Redo Systems: Maintain a history of state snapshots:
class DocumentEditor {
constructor(initialContent) {
this.content = initialContent;
this.history = [structuredClone(initialContent)];
this.currentIndex = 0;
}
edit(changes) {
this.content = { ...this.content, ...changes };
this.history = this.history.slice(0, this.currentIndex + 1);
this.history.push(structuredClone(this.content));
this.currentIndex++;
}
undo() {
if (this.currentIndex > 0) {
this.currentIndex--;
this.content = structuredClone(this.history[this.currentIndex]);
}
}
}
Conclusion & Best Practices
Use structuredClone() as your default deep cloning solution. It handles the vast majority of use cases correctly and requires no dependencies.
Quick reference guide:
- Use
structuredClone()for: complex nested objects, objects with Maps/Sets/Dates, circular references, state management, any scenario requiring true deep copies - Use spread/Object.assign() for: shallow copies, merging objects, when you know the structure is flat
- Use JSON methods for: simple serializable data where performance is critical, data that needs to be sent over network anyway
- Use a library for: cloning class instances while preserving methods, complex custom cloning logic
Remember that structuredClone() cannot clone functions or preserve prototypes. If you need these features, you’ll need to handle them separately or use specialized libraries.
The arrival of structuredClone() as a global API eliminates one of JavaScript’s most persistent pain points. No more importing libraries for basic deep copying, no more JSON workarounds with their edge case failures. For modern JavaScript applications, it’s the right tool for the job.