Copying an object means making a new object with the same properties. A shallow copy makes one new object and copies each property's value as it is. When that value is itself an object or an array, what gets copied is a reference to it, so the copy and the original share the nested object. A deep copy also copies the nested objects, and the objects inside those, all the way down, so it shares nothing with the original.
Picture an address book. A shallow copy is a new book with the same addresses: paint a house you found through the copy, and it's painted for anyone using the original book too. A deep copy would build a new house for every address.
const original = { name: "Ada", address: { city: "London" } };
const copy = { ...original }; // a shallow copy
copy.name = "Grace"; // changes only the copy
copy.address.city = "Paris"; // changes the one address both share
original.name; // "Ada"
original.address.city; // "Paris"
copy.address === original.address; // true
const deep = structuredClone(original); // a deep copy
deep.address.city = "Rome";
original.address.city; // still "Paris"
| Code | Copy | Nested objects |
|---|---|---|
{ ...obj }, Object.assign({}, obj) | Shallow | Shared with the original |
[...arr], arr.slice(), Array.from(arr) | Shallow | Shared with the original |
structuredClone(value) | Deep | Copied |
JSON.parse(JSON.stringify(value)) | Deep, of what JSON can hold | Copied, but dates become strings, properties holding undefined or a function are dropped, and a cycle throws |
structuredClone is built into browsers and Node. It copies dates, Maps, and Sets, and keeps cycles, but it throws on functions and DOM nodes, and a class instance comes back as a plain object without its methods.
The difference matters most with state in React and Redux, which you update by making copies. A shallow copy is a new object, but changing a nested object inside it still changes the old state.
A circular reference is a chain of properties that leads from an object back to itself. It can be direct, as in obj.self = obj, or pass through other objects, as when a team points to its lead and the lead points back to the team. There's nothing wrong with such data, and you can use it normally. The trouble is code that follows every property into every nested object: it goes round the loop and never reaches the end.
const team = { name: "Frontend" };
const ada = { name: "Ada", team };
team.lead = ada; // team -> ada -> team -> ...
ada.team.lead.team.lead.name; // "Ada": you can go round as often as you like
JSON.stringify(team); // throws a TypeError
Here's a walk that visits every nested object, starting from team:
| Step | The walk is at | It follows | Which leads to |
|---|---|---|---|
| 1 | team | team.lead | ada |
| 2 | ada | ada.team | team, where it started |
| 3 | team | team.lead | ada again, and so on forever |
JSON.stringify detects the loop and throws a TypeError, since the text would never end. A function you write that copies, compares, or freezes nested objects, calling itself for each one, doesn't stop on its own: it keeps calling itself until the call stack runs out and the engine throws. So code that walks nested objects has to notice when it comes back round to an object it has already reached.
Reaching an object twice doesn't always mean a loop, though. A shared reference is one object reached by two different paths, with no way back to where you started:
const office = { city: "London" };
const staff = { ada: { office }, grace: { office } };
staff.ada.office === staff.grace.office; // true: one object, two paths
JSON.stringify(staff);
// '{"ada":{"office":{"city":"London"}},"grace":{"office":{"city":"London"}}}'
const copy = structuredClone(staff); // the built-in deep copy
copy.ada.office === copy.grace.office; // true: one new office, still shared
copy.ada.office === office; // false
JSON.stringify writes a shared object out once per path, so parsing the text back gives two separate offices. structuredClone keeps the sharing, and it keeps cycles too: copying team gives a new team whose lead points back to the new team.
Cycles are common in real data. A DOM element's parentNode has the element among its children, tree nodes often link to their parent, a doubly linked list points both ways, and globalThis.globalThis is globalThis.
Write deepClone(value). It returns a copy of value made all the way down, so changing the copy, at any depth, never changes the original.
Copy arrays, plain objects, class instances, Map, Set, Date, and RegExp, all the way down, including Map keys and Set members. Keep each object's prototype, so class instances keep their methods, and copy every own enumerable property, symbol keys included. Primitives and functions come back as they are.
An object that appears twice in the original appears twice in the copy as one copied object, and a cycle becomes the same cycle in the copy.
const original = { when: new Date(0), tags: new Set(["a"]), list: [1, { b: 2 }] };
original.self = original;
const copy = deepClone(original);
copy.self === copy; // true
copy.tags !== original.tags; // true
copy.list[1] !== original.list[1]; // true
copy.when.getTime(); // 0
const original = { a: [1, { b: 2 }], c: { d: "x" } };
const copy = deepClone(original);
expect(copy).toEqual(original);
expect(copy).not.toBe(original);
expect(copy.a).not.toBe(original.a);
expect(copy.a[1]).not.toBe(original.a[1]);
expect(copy.c).not.toBe(original.c);
const original = { name: "root", list: [] };
original.self = original;
original.list.push(original);
const copy = deepClone(original);
expect(copy).not.toBe(original);
expect(copy.self).toBe(copy);
expect(copy.list[0]).toBe(copy);
const shared = { x: 1 };
const copy = deepClone({ a: shared, b: [shared] });
expect(copy.a).toBe(copy.b[0]);
expect(copy.a).not.toBe(shared);
const key = { id: 1 };
const original = new Map([[key, { v: 1 }], ["plain", 2]]);
const copy = deepClone(original);
expect(copy).toBeInstanceOf(Map);
expect(copy).not.toBe(original);
const [[copiedKey, copiedValue]] = copy;
expect(copiedKey).not.toBe(key);
expect(copiedKey).toEqual({ id: 1 });
expect(copiedValue).not.toBe(original.get(key));
expect(copiedValue).toEqual({ v: 1 });
expect(copy.get("plain")).toBe(2);
const key = { id: 1 };
const copy = deepClone({ m: new Map([[key, true]]), s: new Set([key, "x"]) });
expect(copy.s).toBeInstanceOf(Set);
const [member] = copy.s;
expect(member).not.toBe(key);
expect(member).toBe([...copy.m.keys()][0]);
expect(copy.s.has("x")).toBe(true);
const when = new Date(86400000);
const copy = deepClone({ a: when, b: when });
expect(copy.a).toBeInstanceOf(Date);
expect(copy.a).not.toBe(when);
expect(copy.a.getTime()).toBe(86400000);
expect(copy.b).toBe(copy.a);
class Point {
constructor(x) {
this.x = x;
}
double() {
return this.x * 2;
}
}
const original = new Point(2);
const copy = deepClone(original);
expect(copy).toBeInstanceOf(Point);
expect(copy).not.toBe(original);
expect(copy.double()).toBe(4);
const key = Symbol("key");
const original = { re: /a+/gi, [key]: [1] };
const copy = deepClone(original);
expect(copy.re).not.toBe(original.re);
expect(String(copy.re)).toBe("/a+/gi");
expect(copy[key]).toEqual([1]);
expect(copy[key]).not.toBe(original[key]);
const fn = () => 1;
expect(deepClone(fn)).toBe(fn);
expect(deepClone({ fn }).fn).toBe(fn);
expect(deepClone(null)).toBeNull();
expect(deepClone("s")).toBe("s");
expect(deepClone(NaN)).toBeNaN();
expect(deepClone(undefined)).toBeUndefined();
Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.
Ctrl + Enter runs the tests (⌘ + Enter on a Mac).
I'm here to help with Deep clone. I give hints, never the solution, so you get to solve it yourself. Ask for a hint, select code to ask about it, or press Ask Tuto why on a failing test.
Your first 3 answers need no account. for free to keep asking.