Kata: deep clone
Register the copy before you recurse: one seen-map per call closes cycles and keeps shared objects shared.
Predict first
Before you read: predict
ExamHall's offline draft store links each saved answer back to its draft, so a merge can reach the draft from any answer. Before a risky merge it snapshots the draft with this kata answer, then checks the snapshot:
What does it print?
export function cloneDeep(value, seen = new Map()) { if (typeof value !== 'object' || value === null) return value; if (seen.has(value)) return seen.get(value); const copy = Array.isArray(value) ? [] : Object.create(Object.getPrototypeOf(value)); for (const key of Object.keys(value)) copy[key] = cloneDeep(value[key], seen); seen.set(value, copy); return copy;} // The store's snapshot step:const draft = { examId: 'ECON-201', answers: { q7: { choice: 'B' } } };draft.answers.q7.draft = draft;try { const snap = cloneDeep(draft); console.log(`back-link is the snapshot: ${snap.answers.q7.draft === snap}`);} catch (err) { console.log(`snapshot failed: ${err.name}`);}The 30-second version
A kata deep clone returns primitives and functions as they are and copies every object once. One seen-map per call links each source object to its copy; the copy enters the map before its children are copied, so cycles close and shared objects stay shared. Date, Map, Set and arrays get their own constructors; other objects keep their prototype.
Register the copy before you recurse: one seen-map per call closes cycles and keeps shared objects shared.
How it works
Step 1 of 11
Analogy: An adventure gamebook's renumbering table (games)
An editor resets every numbered section of a "turn to 112" gamebook for a new edition. Sections send the reader to other sections, sometimes back to one already read.
| At the editor's desk | In the kata |
|---|---|
| one renumbering table for this edition: old number, new number | the seen-map, one per call (step 3) |
| before setting a section, look it up in the table | seen.has first (step 4) |
| write the new number down as you start a section, before following its "turn to" lines | register, then recurse (step 5) |
| two sections that send readers to 112 both get 112's one new number; no row leaves the table before the edition is done | shared objects stay shared (step 6) |
| maps and colour plates go back to the print shop's own process | Date, Map, Set: their own constructors (step 7) |
| each section keeps its printed template, combat or riddle | the prototype (step 8) |
| the dice-rules page is printed once, and every section points to it | a function is shared, not copied (step 2) |
| a fold-out the shop has no process for comes out as a blank sheet in the right frame | a prototype is not a type (step 9) |
In real terms: a deep clone is a walk over a graph with one table from each original to its copy, filled before the walk goes deeper.
Breaks down when: an editor notices a loop of "turn to" lines and stops; code without the table recurses until the call stack runs out. The table is also keyed by the section itself, by identity, never by its wording: two sections with the same text are still two sections.
Contrasting case: an editor with no table renumbers each section as she meets it. Section 112 gets two new numbers when two sections send readers there, and a section that sends readers back is renumbered forever: a recursive copy with no seen-map.
Examples, run for real
4.1 ExamHall EXH-2850: a shallow snapshot, and the test list
The offline draft store snapshotted the draft with { ...draft } before a risky merge. Spread copies one level (the prerequisite's step 3), so the merge rewrote the snapshot's answer (EXH-2850). MR !3150 replaced it with the reference answer and added the ten-test list. The list also runs on two common mutants: M1 has no seen-map, and M2 makes every plain object with {}.
4.1 EXH-2850 snapshot by { ...draft }: after the merge, the snapshot's q7 reads C
4.1 MR !3150 snapshot by cloneDeep: after the merge, the snapshot's q7 reads B; the draft's reads C
4.1 reference.mjs 10 of 10 pass
4.1 M1, no seen-map 8 of 10 pass · red: T5 "a cycle points back into the copy", T6 "an object reached twice is copied once"
4.1 M2, {} for every object 8 of 10 pass · red: T7 "a class instance keeps its prototype and methods", T10 "an Object.create(null) dictionary keeps its null prototype"
M1 is the seen-map's two jobs seen from the failing side: the cycle and the shared object (steps 3–6). M2 loses the prototype, so the class loses its methods and the dictionary gains a prototype it never had (step 8).
4.2 Payments ledger: a what-if refund on a copy
A ledger's what-if tool clones the ledger and refunds a fee on the copy. The fee posting sits in two accounts' journals, each account links back to the ledger, and the fee's amount is a Money instance with a format() method:
4.2 what-if refund of P-77 on the copy: the copy's fee reads €0.00 through account b; the ledger's still reads €1.20
4.2 one posting in two journals stays one object in the copy: true
4.2 each account's back-link points to the copy: true
4.2 the fee's amount is still a Money with format(): true; its bookedAt a Date: true
4.2 the copy shares no object with the ledger: true
The refund made through account a shows through account b, because both journals hold the posting's one copy (step 6); the back-links close on the copy (step 5); Money keeps its method (step 8). What structuredClone does to the same kind of Money amount is the prerequisite's internals I1.
Try it
Answer first, then check. Nothing is saved.
Question 1 of 8
A chat app clones a sent message before the user edits it. The candidate's kata answer so far:
What does it print?
export function cloneDeep(value, seen = new Map()) { if (typeof value !== 'object' || value === null) return value; if (seen.has(value)) return seen.get(value); if (value instanceof Date) { const copy = new Date(value.getTime()); seen.set(value, copy); return copy; } const copy = Array.isArray(value) ? [] : Object.create(Object.getPrototypeOf(value)); seen.set(value, copy); for (const key of Object.keys(value)) copy[key] = cloneDeep(value[key], seen); return copy;} // The editor's step:const sent = { id: 'm-512', text: 'running late', reactions: new Map([['thumbs-up', ['ana', 'ben']]]),};const draft = cloneDeep(sent);let size;try { size = draft.reactions.size;} catch (err) { size = err.name;}console.log(`reactions: ${draft.reactions instanceof Map} · ${size}`);Question 2 of 8
A video app's watch-party screen copies a party before the host edits it. To keep the map small, this kata answer lets each entry go once its copy is done:
What does it print?
export function cloneDeep(value, seen = new Map()) { if (typeof value !== 'object' || value === null) return value; if (seen.has(value)) return seen.get(value); const copy = Array.isArray(value) ? [] : Object.create(Object.getPrototypeOf(value)); seen.set(value, copy); for (const key of Object.keys(value)) copy[key] = cloneDeep(value[key], seen); seen.delete(value); return copy;} // The screen's step:const host = { name: 'ana' };const party = { title: 'Finale night', host, viewers: [host, { name: 'ben' }] };party.viewers[1].party = party;try { const copy = cloneDeep(party); console.log(`host is the first viewer: ${copy.viewers[0] === copy.host}`);} catch (err) { console.log(`copy failed: ${err.name}`);}Question 3 of 8
A box office's basket keeps an undo snapshot while the seat picker and the checkout form each work on their own copy. Its kata answer keeps the map beside the function, so that the recursive calls can reach it:
What does it print?
const seen = new Map(); export function cloneDeep(value) { if (typeof value !== 'object' || value === null) return value; if (seen.has(value)) return seen.get(value); const copy = Array.isArray(value) ? [] : Object.create(Object.getPrototypeOf(value)); seen.set(value, copy); for (const key of Object.keys(value)) copy[key] = cloneDeep(value[key]); return copy;} // The basket's steps:const basket = { event: 'EV-7', seats: ['K12', 'K13'] };const undo = cloneDeep(basket);const picks = cloneDeep(basket.seats);picks.push('K14');const working = cloneDeep(basket);working.event = 'EV-8';console.log(`undo ${undo.event} · ${undo.seats.length} seats`);Question 4 of 8
ExamHall's proctor console previews a regrade on a copy of a score card. A candidate's kata answer passes primitives through on its first line and has one line left to write, the one for functions:
Which line in the gap makes it print preview: 19/20 pts · card: 18/20 pts?
export function cloneDeep(value, seen = new Map()) { if (Object(value) !== value) return value; /* the function line */ if (seen.has(value)) return seen.get(value); const copy = Array.isArray(value) ? [] : Object.create(Object.getPrototypeOf(value)); seen.set(value, copy); for (const key of Object.keys(value)) copy[key] = cloneDeep(value[key], seen); return copy;} // The console's step:const unit = 'pts';const card = { candidate: 'C-1042', points: 18, max: 20, label: function () { return `${this.points}/${this.max} ${unit}`; },};const preview = cloneDeep(card);preview.points = 19;console.log(`preview: ${preview.label()} · card: ${card.label()}`);Question 5 of 8
the interviewer says: "Implement cloneDeep(value) for plain objects, arrays, Date, Map and Set. Cycles and shared references must survive, class instances keep their class, and functions are not copied." You have 15 minutes and a plain editor, and ExamHall's offline draft store snapshot is the use case.
Before you type: say your test list out loud.
Name at least six tests, each with its input and what it expects.
Question 6 of 8
the interviewer says: "Implement cloneDeep(value) for plain objects, arrays, Date, Map and Set. Cycles and shared references must survive, class instances keep their class, and functions are not copied." You have 15 minutes and a plain editor, and ExamHall's offline draft store snapshot is the use case.
Now write it, in 15 minutes, from a blank file that exports cloneDeep.
Question 7 of 8
A card issuer's dispute desk copies a chargeback case with this kata answer before an agent drafts the reply:
Run against the kata's 10-test list, exactly two tests are red: T5 "a cycle points back into the copy" and T6 "an object reached twice is copied once".
Which one change makes all 10 pass?
export function cloneDeep(value, seen = new Map()) { if (typeof value !== 'object' || value === null) return value; if (seen.has(value)) return seen.get(value); if (value instanceof Date) { const copy = new Date(value.getTime()); seen.set(value, copy); return copy; } if (value instanceof Map) { const copy = new Map(); seen.set(value, copy); for (const [k, v] of value) copy.set(cloneDeep(k, seen), cloneDeep(v, seen)); return copy; } if (value instanceof Set) { const copy = new Set(); seen.set(value, copy); for (const v of value) copy.add(cloneDeep(v, seen)); return copy; } const copy = Array.isArray(value) ? [] : Object.create(Object.getPrototypeOf(value)); seen.set(value, copy); for (const key of Object.keys(value)) copy[key] = cloneDeep(value[key]); return copy;}Question 8 of 8
A shop's price import snapshots each vendor batch before it applies the feed, so it can roll back. The thumbnail is a Uint8Array over its own buffer:
The team's checks: the snapshot shares no object with batch; thumb and the snapshot's copy of it each still hold 4 bytes; the snapshot's rows[0].batch is the snapshot itself.
Which call passes all three?
const thumb = new Uint8Array([137, 80, 78, 71]);const batch = { feed: 'vendor-17', importedAt: new Date(Date.UTC(2026, 9, 4, 6)), prices: new Map([['SKU-1', { cents: 1999 }], ['SKU-2', { cents: 2499 }]]), rows: [{ sku: 'SKU-1', thumb }],};batch.rows[0].batch = batch;const snapshot = /* the call */;Go deeper
Pro tips
- Open with "the seen-map solves two problems: cycles and shared references", then write it. When: the first minute of the kata, after the clarifying questions (step 1). It names the two tests a clone without a seen-map fails, T5 and T6. Verify: mutant M1, with no seen-map, fails exactly T5 and T6, and the reference passes all 10. (The curriculum row's tip, [SELF].)
- Run your own answer against the list before you call it done:
KATA_IMPL=./my-clone.mjs node --test kata.test.mjs. When: practising the kata at home, against the tests the reference passes (step 11). Verify (RAN,tips.mjs):tip 2 KATA_IMPL=./reference.mjs: exit 0 · tests 10 · pass 10 · fail 0 tip 2 KATA_IMPL=<temp>/m2-clone.mjs ({} for every object): exit 1 · tests 10 · pass 8 · fail 2 · red: "T7 a class instance keeps its prototype and methods", "T10 an Object.create(null) dictionary keeps its null prototype"
Source
Vendor and guide pages change, so their claims are labelled as of 2026-10-04; every behaviour this lesson grades is executed on Node 24.19.0.
- MDN, Window: structuredClone() method (the curriculum row's first source), https://developer.mozilla.org/en-US/docs/Web/API/Window/structuredClone (
mdn-structuredclone.txt). It "creates a deep clone of a value using the structured clone algorithm", it "also supports circular references", and transferred objects "are detached from the original object and attached to the new object" (step 10). As of 2026-10-04. - MDN, The structured clone algorithm, https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm (
mdn-structured-clone-algorithm.txt), the page the method links to. The algorithm "clones by recursing through the input object while maintaining a map of previously visited references, to avoid infinitely traversing cycles" (steps 3–5), and its supported types includeTypedArray,RegExpand theErrortypes (step 10). As of 2026-10-04. - Lodash 4.17.15 documentation,
_.cloneDeep(the row's second source), https://lodash.com/docs/4.17.15#cloneDeep. lodash.com refused connections on 2026-10-04 (curl exit 28 at 07:49:50Z, and again at 07:50:25Z), so the page was read from the unmodified Internet Archive capture (id_, Memento-Datetime 2025-12-05T10:14:29Z) of that URL, the treatment_fix-brief-r1.mdruling 1 accepts for a named primary page (lodash-clonedeep.txt, header linesVia,Memento-DatetimeandRaw-SHA256)._.cloneDeep"is like _.clone except that it recursively clones value";_.clonereturns "An empty object … for uncloneable values such as error objects, functions, DOM nodes, and WeakMaps". This lesson's reference is its own code, and returns a function as it is (step 2). - Front End Interview Handbook, JavaScript coding (the row's third source), https://www.frontendinterviewhandbook.com/coding/javascript-utility-function (
feih-javascript-coding.txt, page "Last updated on Aug 13, 2026"). Its basic examples include "Implement Lodash functions: debounce(), throttle(), cloneDeep(), groupBy()"; it says candidates take "around 10-15 minutes for a basic question" (the time box), and advises to "clarify that there are no self-references/cycles within the object/ask if you need to handle them" (step 1). As of 2026-10-04.
Checked against the tools and docs as of 2026-10-04.