How to deep clone a JavaScript object
By Flavio Copes
A complete guide to cloning JavaScript objects with spread, Object.assign(), structuredClone(), transferables, and safe fallbacks.
For most deep-copying jobs in modern JavaScript, start with structuredClone(). But first decide whether you need a copy at all, and whether that copy should be shallow or deep.
Copying objects in JavaScript can be tricky. Some ways perform a shallow copy, which is the default behavior in most of the cases.
Deep copy vs Shallow copy
A shallow copy successfully copies primitive types like numbers and strings, but any object reference will not be recursively copied, but instead the new, copied object will reference the same object.
If an object references other objects, when performing a shallow copy of the object, you copy the references to the external objects.
When performing a deep copy, those external objects are copied as well, so the new, cloned object is completely independent from the old one.
Searching how to deep clone an object in JavaScript on the internet, you’ll find lots of answers but the answers are not always correct.
Use structuredClone() for a deep copy
structuredClone() uses the same structured clone algorithm that browsers use when passing data between windows and workers.
const original = {
name: 'Flavio',
preferences: {
theme: 'dark',
},
}
const cloned = structuredClone(original)
cloned.preferences.theme = 'light'
original.preferences.theme //'dark'
The nested object is independent. This is the main difference from object spread and Object.assign().
Unlike the JSON trick, structuredClone() understands many built-in types:
const original = {
createdAt: new Date(),
tags: new Set(['javascript', 'web']),
prices: new Map([['book', 20]]),
pattern: /guide/gi,
}
const cloned = structuredClone(original)
cloned.createdAt instanceof Date //true
cloned.tags instanceof Set //true
cloned.prices instanceof Map //true
cloned.pattern instanceof RegExp //true
It also handles circular references:
const original = { name: 'circle' }
original.self = original
const cloned = structuredClone(original)
cloned.self === cloned //true
Trying the same thing with JSON.stringify() throws an error.
What structuredClone() cannot copy
Deep clone does not mean “copy every possible JavaScript value.” Functions cannot be cloned:
structuredClone({ run() {} })
//DataCloneError
DOM nodes also cannot be cloned with structuredClone(). A DOM node has its own cloneNode() method, with different rules.
Symbol values are not structured-cloneable either. Private class fields, property descriptors, getters, setters, and the prototype chain are not reproduced as an exact object snapshot.
That last point matters for class instances:
class User {
constructor(name) {
this.name = name
}
greet() {
return `Hi ${this.name}`
}
}
const user = new User('Flavio')
const cloned = structuredClone(user)
cloned instanceof User //false
If an object has behavior, I prefer an explicit copy method or a constructor that accepts plain data. Cloning works best for data, not for arbitrary object graphs full of behavior.
Transfer large buffers instead of copying them
Some values are transferable. Transferring moves their underlying resource to the clone instead of duplicating it.
const buffer = new ArrayBuffer(1024)
const cloned = structuredClone(buffer, {
transfer: [buffer],
})
buffer.byteLength //0
cloned.byteLength //1024
The original buffer becomes detached and can no longer be used. This is useful when sending large binary data to a worker, where avoiding a full copy can save time and memory.
Only transfer when giving up the original is intentional. For normal application objects, omit the transfer option.
Object.assign()
Object.assign() performs a shallow copy of an object, not a deep clone.
const copied = Object.assign({}, original)
Being a shallow copy, values are cloned, and objects references are copied (not the objects themselves), so if you edit an object property in the original object, that’s modified also in the copied object, since the referenced inner object is the same:
const original = {
name: 'Fiesta',
car: {
color: 'blue',
},
}
const copied = Object.assign({}, original)
original.name = 'Focus'
original.car.color = 'yellow'
copied.name //Fiesta
copied.car.color //yellow
Using the Object Spread operator
The spread operator is a ES6/ES2015 feature that provides a very convenient way to perform a shallow clone, equivalent to what Object.assign() does.
const copied = { ...original }
Wrong solutions
Online you will find many suggestions. Here are some wrong ones:
Using Object.create()
Note: not recommended
const copied = Object.create(original)
This is wrong, it’s not performing any copy.
Instead, the original object is being used as the prototype of copied.
Apparently it works, but under the hoods it’s not:
const original = {
name: 'Fiesta',
}
const copied = Object.create(original)
copied.name //Fiesta
original.hasOwnProperty('name') //true
copied.hasOwnProperty('name') //false
See more on
Object.create().
JSON cloning changes data
The JSON technique is not a general clone operation. It is a serialization round trip.
That means it can be acceptable when JSON is the format you intentionally want. For example, an API payload made only from objects, arrays, strings, numbers, booleans, and null can safely cross that boundary after validation.
It is still important to know what changes:
Datebecomes a stringNaNandInfinitybecomenullundefined, functions, and symbol-valued properties disappear from objectsMapandSetlose their entries unless you convert them yourselfBigIntmakesJSON.stringify()throw- circular references make
JSON.stringify()throw
If you want JSON, use JSON. If you want a deep clone of supported JavaScript data, use structuredClone().
Lodash remains a reasonable fallback for an old runtime or when cloneDeepWith() gives you the custom cloning rules your data needs. It should be a deliberate dependency, not the default answer in a modern browser or Node.js application.
Copy only the branch you change
Deep cloning an entire state tree can do much more work than necessary. In many applications we only need to replace the objects along the path being changed:
const user = {
name: 'Flavio',
preferences: {
theme: 'dark',
fontSize: 18,
},
}
const updated = {
...user,
preferences: {
...user.preferences,
theme: 'light',
},
}
updated.preferences is new, while unchanged values can still be shared. This pattern is common in immutable state updates because it preserves identity for the parts that did not change.
For deeply nested state, a data model with smaller objects is often better than cloning everything after every edit.
Cloning does not make data immutable
A clone is still mutable unless you prevent changes separately. Object.freeze() is shallow too:
const settings = Object.freeze({
theme: {
color: 'orange',
},
})
settings.theme.color = 'blue'
The nested object can still change. Copying and immutability solve different problems.
My practical rule
I use object spread for a shallow copy when I am updating one known layer. I use structuredClone() when I have a data object with nested built-in values and need a truly independent copy. I use an explicit serializer or copy function when the data has domain rules, class behavior, or values the structured clone algorithm does not support.
Before cloning, ask what must stay independent. A smaller, intentional copy is easier to reason about than a blind copy of a huge graph.
The platform behavior comes from the HTML Standard’s structured clone algorithm. The related JavaScript object guide explains how references, prototypes, and properties work.
Preserve shared references inside the clone
The structured clone algorithm preserves relationships inside the graph it copies. If two properties point to the same object, their cloned properties point to the same cloned object:
const address = { city: 'Rome' }
const original = {
billingAddress: address,
shippingAddress: address,
}
const cloned = structuredClone(original)
cloned.billingAddress === cloned.shippingAddress //true
cloned.billingAddress === address //false
This is an important difference from a naive recursive function. A recursive clone that does not track objects already visited can duplicate shared values or recurse forever on a cycle.
Property descriptors are not preserved
An object property has more than a value. It can be writable, enumerable, configurable, or implemented by a getter and setter.
Structured cloning creates ordinary data properties in the result. It does not preserve a custom descriptor as a descriptor:
const original = {}
Object.defineProperty(original, 'id', {
value: 42,
writable: false,
enumerable: true,
})
const cloned = structuredClone(original)
const descriptor = Object.getOwnPropertyDescriptor(cloned, 'id')
descriptor.writable //true
If descriptors are part of the behavior you need to preserve, structuredClone() is the wrong abstraction. For a shallow descriptor-preserving copy, you can use:
const copied = Object.create(
Object.getPrototypeOf(original),
Object.getOwnPropertyDescriptors(original)
)
That preserves the current level’s prototype and descriptors, but nested objects remain shared. It is not a deep clone.
Errors and other built-in values
The structured clone algorithm supports many platform types, but the exact set depends on the host environment. Browsers support core JavaScript values plus several Web API objects.
For Error objects, the standard requires useful fields such as name and message to be serialized. Engines are also expected to preserve interesting fields such as stack when possible. Application-specific custom properties should not be treated as a portable error transport format.
If data crosses a worker, iframe, storage, or network boundary, I prefer a deliberate plain-data shape:
const errorData = {
name: error.name,
message: error.message,
code: error.code,
}
This makes the contract visible and avoids depending on engine-specific details.
Test the boundary, not just the happy path
When cloning matters to correctness, add tests for the types your real data contains:
- mutate a nested object and confirm the source does not change
- check whether repeated references stay repeated
- include a date, map, set, or typed array if production data uses it
- confirm unsupported values fail where you expect
- confirm a transferred buffer is detached from the source
A clone that works for a small object literal can still fail when production data gains a function, DOM node, symbol, or class instance.
Avoid home-made general deep-clone functions
A few lines of recursion look attractive until the data contains cycles, symbols, sparse arrays, typed arrays, maps, sets, errors, accessors, property descriptors, or objects from another realm.
Write a custom copier when the domain is small and explicit. For example, copying a Project into a plain export record can be a normal function with known fields. Do not try to recreate the platform’s general cloning algorithm unless that is the project itself.
Related posts about js: