Prototype and Cloning
Every new account was supposed to start the same way: notifications on, an empty list of muted threads. Most did. A handful booted up already muting threads they had never seen, threads that belonged to whoever signed up a few minutes earlier.
The code looked too small to be wrong:
const DEFAULT_PREFS = {
notifications: true,
muted: [],
};
function newAccount(name) {
const prefs = { ...DEFAULT_PREFS };
return { name, prefs };
}
Spread the template, get a fresh copy. That is the mental model, and it is exactly half right. The notifications boolean really is fresh on every account. The muted array is not. { ...DEFAULT_PREFS } copied the reference to the one and only muted array that lives on the template, so every account’s prefs.muted points at the same array in memory. The first time anyone runs account.prefs.muted.push(threadId), they push into the shared template, and every account created after that starts with the thread already muted.
One template. One array. Hundreds of accounts quietly sharing it.
This turns out to be two topics that are really the same topic. The prototype pattern is the idea of making new objects by cloning a known-good example instead of building each one from nothing. Cloning is the mechanics underneath, and JavaScript hands you three or four ways to do it that all break differently. The pattern is only ever as correct as the copy, so we are going to spend most of our time on the copy.
Cloning a template, on purpose
The prototype pattern, stripped of ceremony: keep one fully configured object around, and when you need another, copy it. That is the whole idea. The “prototype” is your trusted example, and every new object is a clone of it.
The classic reason is expensive construction. If assembling an object means parsing a big file, walking a schema, or wiring defaults you validated once, do that work a single time to build the template, then clone the template whenever you need a fresh instance. Copying a finished object is almost always cheaper than running the constructor again.
The more common reason in application code is “new things must start identical to a vetted default.” Default configs. A game spawning a hundred enemies from one prototype. Test fixtures that all begin from the same known-good record and each tweak one field. In every case there is a template you trust and a copy you are free to modify.
In a classic object-oriented language the pattern comes with baggage. Every class implements a clone() method, usually declared by a Cloneable interface, because the language has no built-in way to duplicate an arbitrary object. JavaScript skips all of that. Copying an object is a language-level operation, so the pattern here is mostly the discipline of keeping a template and copying it correctly. There is no interface to implement and no clone() to write, right up until your objects have behavior, which is a wrinkle we will get to.
The language really does have prototypes
The pattern’s name is not a coincidence. JavaScript’s entire object model is built on prototypes, and one of its core operations is exactly “make a new object based on an existing one.”
Object.create(proto) builds a new object whose prototype is proto and whose own set of properties is empty:
const enemyTemplate = {
role: "guard",
hp: 100,
describe() {
return `${this.role} with ${this.hp} hp`;
},
};
const goblin = Object.create(enemyTemplate);
goblin.role = "goblin"; // an own property, shadows the template
goblin.describe(); // "goblin with 100 hp"
goblin.hp; // 100, read straight off the template
Read the important words: based on, not copied from. Object.create does not duplicate anything. It links. goblin starts empty and delegates every read it cannot answer to enemyTemplate through the prototype chain. Ask for goblin.hp, find no own hp, and the engine walks up to the template and returns 100. Change enemyTemplate.hp later and goblin.hp changes with it, because the goblin never had its own copy.
That is the crucial difference from the opening. Spread and structuredClone take a snapshot: they read the template’s properties now and write them onto a brand-new independent object. Object.create takes a link: the new object shares the template live, forever, through the chain.
Which one you want depends on the job. A live link is right when the template is a shared bag of defaults and methods that many objects should see, where each object only stores its own overrides. That is prototypal inheritance doing its actual job, and Object.create is how you reach for it directly without a class. A snapshot is right when the new object must be able to drift from the template without dragging it along, which describes most application data.
There is a catch that rhymes with the opening bug. Delegation only helps for reads. Write a primitive and you create an own property that shadows the template, which is safe. But reach through the chain into a shared nested object and mutate it, and you have edited the template for everyone:
const base = { tags: ["core"] };
const a = Object.create(base);
a.tags.push("beta"); // no own "tags" to shadow, so this walks up to base
base.tags; // ["core", "beta"] ← the template changed
Same class of bug as the spread. A link and a shallow copy both leave nested objects shared, just by different routes.
Shallow versus deep, which is the whole game
Nearly every real cloning bug is one bug: you made a shallow copy and treated it like a deep one.
A shallow copy duplicates the top level and stops. Spread and Object.assign are both shallow:
const original = {
title: "draft",
tags: ["a", "b"],
};
const clone = { ...original };
clone.title = "final"; // fine: title is a string, copied by value
clone.tags.push("c"); // not fine: tags is the SAME array as original.tags
original.title; // "draft" ← independent, as expected
original.tags; // ["a", "b", "c"] ← shared, surprise
The primitive title behaves. Strings, numbers and booleans copy by value, so the clone’s copy is genuinely its own. The tags array copies by reference: the clone gets a pointer to the same array, and mutating through either name changes the one array they share.
Reach one level deeper and you need a deep copy: one that walks the whole tree and duplicates every nested object and array, so the clone shares nothing with the original. structuredClone is the built-in that does this, and in 2026 it is everywhere worth targeting (every current browser, plus Node 17 and up):
const clone = structuredClone(original);
clone.tags.push("c");
original.tags; // ["a", "b"] ← untouched
Now the arrays are two different arrays. Push into one and the other never notices.
Play with the difference below. Pick a copy mode, then spawn clones from the template and watch what happens to the template’s own list. In shallow mode every clone is pushing into the template’s array, so the “original” grows with each spawn and all the clones show the same array. Flip to deep and the template never moves.
The tools, and exactly how each one lets you down
Three ways to copy, three different failure modes. This is the short version aimed at the pattern; the full breakdown has the exhaustive list.
| technique | depth | Map, Set, Date | functions | cycles |
|---|---|---|---|---|
| spread, Object.assign | shallow (top level only) | shared by reference | shared by reference | fine, no recursion |
| structuredClone | deep | rebuilt correctly | throws DataCloneError | rebuilt correctly |
| JSON round-trip | deep, data only | corrupted silently | dropped silently | throws |
Spread and Object.assign are shallow, and that is the whole story. Fast, fine for a flat object, wrong the instant there is a nested object you intend to mutate.
structuredClone is a genuine deep copy and the right default when you need one. It rebuilds Map, Set, Date, RegExp, typed arrays and even circular references correctly. It has exactly one hard rule that bites: it copies data, not behavior. Hand it a function anywhere in the tree and it throws DataCloneError rather than guessing.
The old trick, JSON.parse(JSON.stringify(x)), is the one to retire. It looks like a deep clone and mostly acts like one, until a Date silently becomes a string, a Map becomes {}, undefined values and functions vanish without a word, and a cycle throws outright. It survives in codebases because it is faster than structuredClone for plain data and it predates it by a decade. If your object really is nothing but strings, numbers, arrays and plain objects, it works and it is quick. The moment it is not, it corrupts your data quietly, which is the worst way to be wrong.
When the template has methods
Here is the part the copy catalogue in Part 1 does not dwell on, because it is really a pattern problem. All three techniques above hand you back a plain object. None of them preserves the prototype.
That does not matter when your template is a bag of data. It matters a great deal when your template is a live class instance:
class Vec {
constructor(x, y) { this.x = x; this.y = y; }
add(o) { return new Vec(this.x + o.x, this.y + o.y); }
}
const v = new Vec(1, 2);
const copy = structuredClone(v);
copy.x; // 1 ← data survived
copy instanceof Vec; // false ← the prototype did not
copy.add; // undefined ← add lived on Vec.prototype
structuredClone walked the own data properties (x and y) and rebuilt them on a fresh object whose prototype is plain Object.prototype. The add method was never an own property. It lived on Vec.prototype, and the clone has no link to it. copy is a data-shaped ghost of a Vec that cannot do anything a Vec does.
This is the cleanest argument in the whole lesson for keeping data and behavior apart. If your template is plain data and your behavior lives in free functions or a factory, cloning is trivial and total: copy the data, done. If your template is a rich instance, you have three honest options, in rough order of preference:
- Separate the two. Deep clone the plain data, then wrap it:
Object.create(Vec.prototype)with the data assigned on, or pass the data through a factory that reattaches behavior. The clone is data, the behavior is shared. - Give the class a real
clone(). A method that knows how to rebuild an instance of itself (return new Vec(this.x, this.y)) is the classic prototype pattern, honest about what it produces. More code, full control. - Do not clone instances at all. Often the right answer. Keep instances as identities you pass around, and clone only the plain data they hold.
The mistake is expecting a general-purpose deep copy to resurrect a class. It copies state. Behavior is your job.
Immutability, the other way out
Step back and notice why we clone at all. We clone because we want to change something without disturbing the thing we copied it from. The entire shallow-versus-deep minefield exists to serve “copy, then mutate.”
There is a different move: do not mutate. If nothing ever changes a shared object in place, sharing it is completely safe and you never needed the deep copy in the first place.
That flips clone-then-mutate into copy-on-write. Instead of duplicating the world defensively and editing the duplicate, you produce a new value for the one thing that changed and share everything else by reference:
// not this: deep clone the whole tree just to edit one field
const next = structuredClone(state);
next.user.name = "Ada";
// this: a new object for what changed, the rest shared and never mutated
const next = { ...state, user: { ...state.user, name: "Ada" } };
The second version copies two small objects and shares every other branch of state untouched. It is cheaper than a deep clone, and it is safe for exactly one reason: nobody mutates the shared branches. That contract is the price of admission. Immutable array methods like toSorted, with and toSpliced exist to make this easy for arrays, returning a new array instead of editing in place, and this new-object-per-change discipline is the model every serious state library is built on.
You can enforce the “nobody mutates it” half with Object.freeze, which makes a template genuinely tamper-proof:
const DEFAULT_PREFS = Object.freeze({ notifications: true });
DEFAULT_PREFS.notifications = false; // ignored in loose mode, throws in strict mode
One honest caveat, and by now you can guess it: Object.freeze is shallow, exactly like spread. It freezes the top level and leaves nested objects mutable, so a frozen template with a nested array still has a pushable array unless you freeze that too. Same one-level-deep story, one more time.
So do you actually need a deep clone?
Most of the time, no, and reflexively reaching for structuredClone on every object is its own small waste. The decision is not hard once you name it.
Deep clone when all three are true: the data is nested, you are going to mutate the copy, and the original has to stay exactly as it was. That is the defensible case, and it is real. Snapshotting state before an edit. Handing a caller a copy they can wreck without consequence. Capturing an audit record of an object that will keep changing under you.
Do not deep clone when any of these hold. If the object is flat, a spread is enough and a deep clone is theater. If you never mutate, share the reference and skip the copy entirely, which is both cheaper and safer. If you are in a hot path duplicating a large object every frame or every request, that deep clone is now your performance problem, and sharing the parts everyone has in common (the idea behind the flyweight pattern) is the way out.
The pattern is a good servant and a bad master. A template you clone is a clean way to make many things that start identical. A reflex to deep-copy everything, just in case, is how you turn a reference bug you would have caught into a performance bug you will not.
Summary
- The prototype pattern makes new objects by cloning a known-good template instead of constructing each from scratch. It pays off when construction is expensive, or when new objects must start identical to a vetted default (configs, game entities, fixtures).
- In JavaScript the pattern is mostly discipline, not machinery. Copying is built in, so there is no
Cloneableinterface to implement, only a template to keep and a copy to get right. Object.create(proto)links a new object to a template through the prototype chain (live, shared reads), while spread andstructuredClonetake an independent snapshot. Different tools for different jobs.- The bug behind almost every cloning surprise is shallow versus deep. Spread,
Object.assignandObject.freezeall stop at the top level, so nested objects and arrays stay shared and a mutation on the copy reaches the original. structuredCloneis the built-in deep copy and the right default when you need one. It rebuilds Maps, Sets, Dates and cycles, but throws on functions and discards the prototype, so a cloned class instance comes back as a plain object with its methods gone.- Retire
JSON.parse(JSON.stringify(x)). It drops functions andundefined, turns Dates into strings and Maps into{}, and throws on cycles. Fine for pure plain data, silently corrupting for anything else. - Cloning copies state, not behavior. If a template has methods, separate the data from the behavior, give the class a real
clone(), or do not clone the instance at all. - The alternative to clone-then-mutate is do not mutate: copy-on-write a new object for what changed and share the rest. Cheaper than a deep clone, and safe as long as nothing touches the shared parts.
- When to use: a trusted template copied to produce many independent objects. When to avoid: flat data (spread is enough), data you never mutate (share it), or a hot path where a reflexive deep clone becomes the bottleneck.