Use the built-in structuredClone(value). It copies every level of nested objects and arrays, keeps Date, Map and Set intact, and handles circular references. { ...obj } and Object.assign() only copy the top level.
Objects and arrays in JavaScript are passed around by reference, so const b = a gives you a second name for the same object, not a copy. A shallow copy makes a new outer object but still shares everything nested inside it. A deep clone copies all the way down, so nothing you do to the copy can reach the original. For years the answers were a JSON round trip or lodash's cloneDeep. Today the language has structuredClone built in. Each example runs on this page: hit Run, then edit the code and run it again.
1structuredClone()Recommended
structuredClone is a global function, so there is nothing to import. It walks the whole value and builds an independent copy: nested objects, arrays, Date, Map, Set, RegExp, typed arrays and circular references all come through. It works on arrays just as well as on objects.
Output
The first line, [ 'admin', 'dev' ] London write, is the original, untouched by any of the three edits. The copy prints [ 'admin', 'dev', 'ops' ] Paris read. The date is still a real Date (true 2026-09-24T00:00:00.000Z) but a different object, so the last line is false. structuredClone is available in every current browser, Node 17+, Deno and Bun.
2Shallow copies: spread, Object.assign and array copies
{ ...obj } and the older Object.assign({}, obj) create a new object and copy each top-level property into it. For arrays, [...arr], arr.slice() and Array.from(arr) do the same. That is enough when the values are strings and numbers. When a value is itself an object or array, only the reference gets copied, so the nested part is shared.
Output
true false confirms that alias is the same object and spread is not. But user now prints Ada Paris: renaming the copy left the original's name alone, while changing the nested city changed both, because true true shows both copies share the one address object. The array half is the same story. Pushing a row only grew the copy, but rows[0][0] = 99 reached the original too, which now prints [ [ 99, 2 ], [ 3, 4 ] ]. A shallow copy is the right tool for a flat object or an array of primitives. Anything nested needs section 1.
3JSON.parse(JSON.stringify()) and what it loses
Before structuredClone, the common one-liner was to serialize to a JSON string and parse it back. It does produce a deep copy, but only of what JSON can represent. Everything else is converted or dropped without an error, and a circular reference throws.
Output
Read the printed copy line by line. when came back as the string '2026-09-24T00:00:00.000Z' (typeof prints string), missing vanished entirely, NaN became null, the Map and Set both became empty {}, and the undefined inside the array turned into null: [ 1, null, 3 ]. The cycle throws TypeError: Converting circular structure to JSON, while structuredClone copies it and prints root. The round trip is still fine for data that is already plain JSON, such as an API response.
4What structuredClone cannot clone
structuredClone copies data, not behaviour. A class instance comes back as a plain object with the same own properties but without its prototype, so its methods are gone. A function anywhere in the value makes the whole call throw a DataCloneError.
Output
The copy prints as { x: 3, y: 4 }, not Point { x: 3, y: 4 }, and false undefined confirms the prototype and the length method are gone. The object with a method throws DataCloneError: onClick() {} could not be cloned. Copying the cloned data onto a fresh instance restores the class: true 5. For classes you own, a clone() method that calls the constructor is usually clearer. Libraries such as lodash's cloneDeep do keep prototypes, if you already depend on one.
5Which should you use?
| Method | Depth | Date / Map / Set | Cycles | Best for |
|---|---|---|---|---|
| structuredClone(obj) | Deep | Kept | Kept | Almost every deep copy |
| { ...obj } / [...arr] | One level | Shared, not copied | Shared | Flat objects, arrays of primitives |
| Object.assign({}, obj) | One level | Shared, not copied | Shared | Older code; copying onto an existing object |
| JSON.parse(JSON.stringify(obj)) | Deep | Lost (string / {}) | Throws TypeError | Data that is already plain JSON |
6Common variation: copy only the path you change
Often you don't need a full deep clone. In immutable state updates (React, Redux and similar), the pattern is to spread each level on the way down to the one value you change and share everything else. Unchanged parts keep their identity, which is what lets memoized code skip them.
Output
Prints London Paris and false true: the original state is untouched. The last line, true, shows the todo that didn't change is the very same object in both states. That is cheaper than structuredClone(state) and tells the rest of your code exactly which parts changed.
Frequently asked questions
What is the most efficient way to deep clone an object in JavaScript?
Use structuredClone(obj). It is built into every current browser, Node 17+, Deno and Bun, needs no library, and copies nested objects, arrays, Date, Map, Set and circular references correctly. JSON.parse(JSON.stringify(obj)) can be competitive on plain JSON data, but it silently breaks dates, undefined, Map and Set, so measure before trading correctness for speed.
Does the spread operator make a deep copy?
No. { ...obj } and [...arr] are shallow: they copy the top-level properties or elements into a new object, but a nested object or array is copied by reference and stays shared with the original. Object.assign({}, obj) behaves the same way. Use structuredClone when the value has anything nested.
How do I copy an array instead of a reference?
const b = a only creates a second name for the same array. For a new array use [...a], a.slice() or Array.from(a). Those are shallow copies, which is enough for an array of numbers or strings. For an array of objects or arrays, use structuredClone(a) so the inner items are copied too.
Why is JSON.parse(JSON.stringify(obj)) a bad way to clone?
It only keeps what JSON can represent. A Date comes back as a string, properties set to undefined disappear, undefined in an array becomes null, NaN and Infinity become null, Map and Set become {}, functions are dropped, and a circular reference throws TypeError: Converting circular structure to JSON.
Can structuredClone copy functions or class instances?
Not fully. A function anywhere in the value throws a DataCloneError. A class instance is copied as a plain object: its own data properties survive but the prototype does not, so instanceof returns false and methods are missing. Rebuild it with Object.assign(new MyClass(), structuredClone(obj)) or give the class its own clone() method.
Do I still need lodash cloneDeep?
Usually not. structuredClone covers plain objects, arrays, dates, maps, sets and cycles without a dependency. Lodash cloneDeep is still useful if you need class instances to keep their prototype, or if you already use lodash and want one consistent behaviour across old runtimes.
Run it yourself
Open any of these in the full JavaScript editor to tweak, run and share.
JavaScript playground