What you'll be able to do
By the end of this page you should be able to explain shallow vs deep copy, use spread/slice when only the top level must be new, and reach for structuredClone when nested data must not stay shared.
Previous post: object mutation and shared state.
Who this is for
- People burned by
{ ...user }still sharinguser.address - Beginners who clone with
JSON.parse(JSON.stringify(x))without knowing the limits - Anyone copying settings objects with nested maps
You can skip this if you already pick shallow vs deep on purpose. Come back when a nested edit leaks across “copies.”
Shallow copy: new shell, shared insides
const original = {
name: "Asha",
address: { city: "Pune" },
};
const shallow = { ...original };
shallow.name = "Ben"; // original.name stays "Asha"
shallow.address.city = "Delhi"; // original.address.city also "Delhi"
Scenario A — flat fields only. Shallow is enough and cheap.
Scenario B — nested address / meta / arrays of objects. Shallow is not isolation.
Arrays: arr.slice() / [...arr] are shallow too — nested objects inside the array remain shared.
Deep copy: duplicate the tree
const original = {
name: "Asha",
address: { city: "Pune" },
};
const deep = structuredClone(original);
deep.address.city = "Delhi";
console.log(original.address.city); // "Pune"
Scenario A — clone form state before editing a draft.
Scenario B — older habit. JSON.parse(JSON.stringify(obj)) deep-copies plain data but drops functions, undefined, Dates become strings, and can throw on cycles. Prefer structuredClone for modern beginners when available.
When not to deep-clone everything
Deep cloning big trees on every keystroke is expensive. Often you only need a new top-level object and a new nested object for the branch you change:
const next = {
...original,
address: { ...original.address, city: "Delhi" },
};
Scenario A — update one nested field in UI state. Nested spread is enough.
Scenario B — unknown deep tree. structuredClone is simpler and safer than hand-spreading ten levels.
A tiny practice file
Save as copy-depth.html.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>shallow vs deep</title>
</head>
<body>
<button id="shallow">Shallow copy edit</button>
<button id="deep">Deep copy edit</button>
<pre id="out"></pre>
<script>
const out = document.getElementById("out");
function sample() {
return { name: "Asha", address: { city: "Pune" } };
}
document.getElementById("shallow").addEventListener("click", function () {
const original = sample();
const copy = { ...original };
copy.address.city = "Delhi";
out.textContent = "shallow → original city = " + original.address.city;
});
document.getElementById("deep").addEventListener("click", function () {
const original = sample();
const copy = structuredClone(original);
copy.address.city = "Delhi";
out.textContent = "deep → original city = " + original.address.city;
});
</script>
</body>
</html>
Mistakes I see a lot
1. Calling every spread a deep clone.
2. JSON clone without knowing Date/undefined/function loss.
3. Deep-cloning on every tiny update. Prefer targeted nested spreads when you can.
4. Forgetting arrays of objects are nested.
5. Cloning class instances blindly. structuredClone has limits with some exotic types — stick to plain data as a beginner.
6. Mutating after clone and testing the wrong variable. Log original on purpose.
What to try before the next post
- Reproduce shallow nested leak once.
- Fix it with
structuredClone. - Fix the same case with nested spreads.
- Build the tiny HTML file.
Next in this series: type coercion surprises — {} === {}, [] === [], and "5" + 1 vs "5" - 1.
Try this next outside the series
Objects and references click faster when you connect them to state, payloads, and database-shaped data.
- MERN MongoDB modeling — see objects turn into real documents and nested structures
- JSON Formatter — inspect deep objects before you mutate or copy them