Copying an object in JavaScript looks simple until the object has nested objects or arrays inside it — then the most common copying tricks quietly break, leading to one of the most frequent real-world JavaScript bugs: mutating a "copy" accidentally changes the original object too.
Why Shallow Copies Aren't Enough
Both the spread operator and Object.assign only copy the top level of an object. Any nested object or array is copied by reference, not by value:
const original = {
name: "Alice",
address: { city: "Mumbai", zip: "400001" },
};
const shallowCopy = { ...original };
shallowCopy.address.city = "Pune"; // intended to only change the copy
console.log(original.address.city); // the "original" changed too!
Output:
Pune
Both original.address and shallowCopy.address point to the exact same nested object in memory, so modifying one modifies the other. A true deep clone needs to recursively copy every nested object and array, not just the outer layer.
Deep Clone Function in JavaScript
function deepClone(value) {
// primitives (and null) don't need cloning
if (value === null || typeof value !== "object") return value;
if (Array.isArray(value)) {
return value.map((item) => deepClone(item));
}
const clonedObject = {};
for (const key in value) {
if (Object.hasOwn(value, key)) {
clonedObject[key] = deepClone(value[key]); // recurse into nested values
}
}
return clonedObject;
}
const user = {
name: "Alice",
address: { city: "Mumbai", zip: "400001" },
hobbies: ["reading", "chess"],
};
const deepCopy = deepClone(user);
deepCopy.address.city = "Pune";
deepCopy.hobbies.push("cycling");
console.log(user.address.city); // unaffected
console.log(user.hobbies); // unaffected
console.log(deepCopy.address.city);
console.log(deepCopy.hobbies);
Output:
Mumbai
[ 'reading', 'chess' ]
Pune
[ 'reading', 'chess', 'cycling' ]
The Modern Built-In: structuredClone()
Current JavaScript runtimes (modern browsers and Node.js 17+) ship a built-in structuredClone() function that does a proper deep clone for you, including handling of Date, Map, and Set objects that a hand-written recursive clone wouldn't automatically support:
const copy = structuredClone(user);
copy.address.city = "Delhi";
console.log(user.address.city); // still "Mumbai"
console.log(copy.address.city);
Output:
Mumbai
Delhi
Writing your own deepClone is a great exercise for understanding recursion over object trees, but for production code, prefer the built-in structuredClone() — it's well-tested and handles edge cases (circular references, more data types) that a basic recursive version doesn't. Note that it can't clone functions or DOM nodes, so for objects containing those, a library like Lodash's cloneDeep is a safer choice.
