A deep clone makes a copy of a JavaScript value such that the copy has no shared references to nested arrays or objects in the original. Mutating the cloned value should never affect the original.
In production code, use the built-in structuredClone():
const original = { user: { name: 'Ada', tags: ['admin'] } };const copy = structuredClone(original);copy.user.tags.push('owner');console.log(original.user.tags); // ['admin']console.log(copy.user.tags); // ['admin', 'owner']
structuredClone is part of the Web Platform spec. It is available in all evergreen browsers, Node.js 17+, and Deno. It correctly handles plain objects and arrays as well as Date, RegExp, Map, Set, ArrayBuffer, typed arrays, and circular references. None of those work with the older JSON.parse(JSON.stringify(value)) trick.
structuredClone isn't enoughstructuredClone covers most cases, but it throws a DataCloneError for values that cannot be structured-cloned and silently flattens others:
| Input | Behavior |
|---|---|
| Functions / methods | Throws DataCloneError. |
| DOM nodes | Throws (except via the special transfer option for transferable types). |
| Symbol values | Throws. |
| Class instances | Cloned as plain objects. The prototype is not preserved. |
| Getters and setters | Flattened to data properties on the clone. |
| Property descriptors | enumerable, writable, configurable flags are not preserved. |
If you need to clone any of those, or you need full control over what gets shared versus copied, you'll write your own deepClone. Implementing it from scratch is also one of the most common JavaScript interview questions, because it tests recursion, type detection, and Object traversal in a small surface area.
Looking for the conceptual explanation of "shallow vs deep copy"? See the dedicated quiz page: Explain the difference between shallow copy and deep copy.
Implement deepClone(value) so it returns a deep copy of a JSON-serializable value. The input may be null, booleans, numbers, strings, arrays, or plain objects. It will not contain cycles or special objects like Date, RegExp, Map, or Set. Primitive values can be returned as-is.
value (*): The value to clone.(*): Returns a deep copy of value.
const obj1 = { user: { role: 'admin' } };const clonedObj1 = deepClone(obj1);clonedObj1.user.role = 'guest'; // Change the cloned user's role to 'guest'.clonedObj1.user.role; // 'guest'obj1.user.role; // Should still be 'admin'.const obj2 = { foo: [{ bar: 'baz' }] };const clonedObj2 = deepClone(obj2);obj2.foo[0].bar = 'bax'; // Modify the original object.obj2.foo[0].bar; // 'bax'clonedObj2.foo[0].bar; // Should still be 'baz'.
Writing out a complete deep clone solution from scratch is almost impossible under typical interview constraints. In typical interview settings, the scope is fairly limited, and interviewers are more interested in how you would detect different data types and your ability to leverage various built-in APIs and Object methods to traverse a given object.
For interview purposes, Approach 2 is the one to learn. Approach 1 is useful as a baseline to discuss, but it only works for JSON-safe data and avoids the actual cloning logic.
JSON.stringifyThe easiest (but flawed) way to deep copy an object in JavaScript is to first serialize it and then deserialize it back via JSON.stringify and JSON.parse.
export default function deepClone(value) {return JSON.parse(JSON.stringify(value));}
Although this approach is acceptable given the input object only contains null, boolean, number, string, you should be aware of its downsides:
JSON.stringify also has a few other surprising behaviors such as converting Date objects to ISO timestamp strings, and turning NaN and Infinity into null.Obviously, your interviewer will not allow you to use this.
This is the recommended solution. Traverse the value recursively, cloning arrays element-by-element and plain objects property-by-property.
/*** @template T* @param {T} value* @return {T}*/export default function deepClone(value) {if (typeof value !== 'object' || value === null) {// Primitives can be returned directly because they are already immutable values.return value;}if (Array.isArray(value)) {// Clone each slot so nested arrays do not share references with the original.return value.map((item) => deepClone(item));}// Rebuild the object with recursively cloned property values.return Object.fromEntries(Object.entries(value).map(([key, value]) => [key, deepClone(value)]),);}
There are generally two ways we can traverse an object:
for ... in statement.Object.keys(), or an array of key-value tuples with Object.entries().With the for ... in statement, inherited enumerable properties are processed as well. On the other hand, Object.keys() and Object.entries() only care about the properties directly defined on the object, and this is usually what we want.
We will address these edge cases in Deep Clone II.
structuredClone (the modern one-liner)For production code outside an interview, the right answer is the built-in structuredClone. It is available in all evergreen browsers, Node.js 17+, and Deno.
const clonedObj = structuredClone(obj);
What structuredClone handles correctly that JSON.parse(JSON.stringify(value)) does not:
Date, RegExp, Map, Set.ArrayBuffer, typed arrays, Blob, File, FileList, ImageData.What it still does not handle:
| Input | Behavior |
|---|---|
| Functions / methods | Throws DataCloneError. |
| DOM nodes | Throws (except via the transfer option for transferable types). |
Symbol values | Throws. |
| Class instances | Cloned as plain objects. The prototype is not preserved. |
| Getters and setters | Flattened to data properties on the clone. |
Property descriptors (enumerable, writable, configurable) | Not preserved. |
See "Deep-copying in JavaScript using structuredClone" on web.dev for the full reference.
When you reach for a deep clone, work down this short decision list:
JSON.parse(JSON.stringify(value)) is fastest for that narrow case. Watch for the Date and NaN/Infinity pitfalls noted in Approach 1.structuredClone(value). This is the right default for most apps.cloneDeep.useReducer with structural updates instead.console.log() statements will appear here.