Prototype creates new objects by copying an existing one instead of building them from scratch. You configure a template once, then clone it whenever you need another, and adjust only what differs. A mould for casting is the physical version: designing the mould is expensive, every cast after that is cheap and identical.
Difficulty: ★★☆☆☆ for the idea, ★★★☆☆ once you meet copying bugs. Popularity: ★★★☆☆. In JavaScript you use it constantly without the name every time you write { ...defaults, ...overrides }.
Why copy instead of construct?
In JavaScript, new is cheap, so the reason for Prototype is rarely speed. The real reasons are:
- The configured state is expensive to produce. A template that took a database query, a config merge or a heavy computation to build is worth copying rather than rebuilding.
- You do not know the concrete class. Code that receives "some unit" or "some document template" can ask it to
clone()itself without aswitchover its type. - You want many variants of one baseline. A base template plus small overrides is easier to maintain than twenty constructors with twenty argument lists.
The running example is a strategy game that spawns units from templates: archers, knights, healers, each with stats and skills tuned by designers.
The problem: the template that changes behind your back
The obvious first version copies the template with spread:
type Stats = { hp: number; attack: number; defense: number };
type UnitData = { name: string; stats: Stats; skills: string[] };
const archerTemplate: UnitData = {
name: "Archer",
stats: { hp: 80, attack: 12, defense: 4 },
skills: ["volley"],
};
const eliteArcher = { ...archerTemplate, name: "Elite archer" };
eliteArcher.stats.attack += 5;
eliteArcher.skills.push("piercing-shot");
console.log(archerTemplate.stats.attack, archerTemplate.skills);
// 17 [ "volley", "piercing-shot" ]
The elite archer is fine. The template is now broken: every archer spawned from this point on has 17 attack and a skill it should not have. Spread and Object.assign copy only the top level. stats and skills are references, so the "copy" shares them with the original.
This is the most important thing to know about Prototype in JavaScript, and the bug is common in real code: default config objects mutated by one request, Redux state "copied" with spread and then mutated, test fixtures that leak between tests.
Deep copies with structuredClone
structuredClone, global in Node.js 17+ and every modern browser, makes a real deep copy:
const knightTemplate: UnitData = {
name: "Knight",
stats: { hp: 150, attack: 10, defense: 12 },
skills: ["shield-wall"],
};
const captain = structuredClone(knightTemplate);
captain.name = "Knight captain";
captain.stats.hp += 50;
captain.skills.push("rally");
console.log(knightTemplate.stats.hp, knightTemplate.skills); // 150 [ "shield-wall" ]
It also handles things a JSON round trip gets wrong: Date, Map, Set, typed arrays and circular references all survive. But it has limits you need to know:
- Class instances come back as plain objects. Methods and the prototype are lost.
- Functions cannot be cloned and throw a
DataCloneError. - Accessors turn into plain data. An own getter is called once and only its current value is copied, a setter-only property becomes
undefined, and#privatefields and getters defined on a class are dropped.
So structuredClone is perfect for data, and not enough for objects with behaviour. That is where the pattern proper comes in.
The pattern: objects that know how to clone themselves
Give the type a clone() method. The object knows its own structure, so it can copy itself correctly, including the parts that need care:
let nextId = 1;
class Unit {
readonly id = nextId++;
constructor(
public name: string,
public stats: Stats,
public skills: string[],
public readonly faction: string,
) {}
clone(changes: Partial<Pick<Unit, "name">> = {}): Unit {
// Deep-copy the data, keep the class, and give the copy its own identity.
return new Unit(changes.name ?? this.name, structuredClone(this.stats), [...this.skills], this.faction);
}
levelUp() {
this.stats.attack = Math.round(this.stats.attack * 1.1);
this.stats.hp = Math.round(this.stats.hp * 1.1);
}
describe() {
return `#${this.id} ${this.name} (${this.faction}) hp=${this.stats.hp} atk=${this.stats.attack} [${this.skills.join(", ")}]`;
}
}
The id is an important detail: a clone is a new object and must not share identity fields (IDs, created-at timestamps, database keys) with its template. Decide explicitly which fields are copied, which are deep-copied and which are reset.
A prototype registry
Templates are usually looked up by key. A registry holds the configured prototypes and hands out clones, so game code never constructs units directly:
class UnitRegistry {
private readonly prototypes = new Map<string, Unit>();
register(key: string, prototype: Unit) {
this.prototypes.set(key, prototype);
}
spawn(key: string, customize?: (unit: Unit) => void): Unit {
const prototype = this.prototypes.get(key);
if (!prototype) throw new Error(`Unknown unit: ${key}`);
const unit = prototype.clone();
customize?.(unit);
return unit;
}
}
const registry = new UnitRegistry();
// In a real game these would be loaded once from designer-edited config files.
registry.register("archer", new Unit("Archer", { hp: 80, attack: 12, defense: 4 }, ["volley"], "Sun"));
registry.register("knight", new Unit("Knight", { hp: 150, attack: 10, defense: 12 }, ["shield-wall"], "Sun"));
const a = registry.spawn("archer");
const b = registry.spawn("archer", (unit) => {
unit.name = "Veteran archer";
unit.levelUp();
unit.skills.push("piercing-shot");
});
const c = registry.spawn("archer");
console.log(a.describe()); // #3 Archer (Sun) hp=80 atk=12 [volley]
console.log(b.describe()); // #4 Veteran archer (Sun) hp=88 atk=13 [volley, piercing-shot]
console.log(c.describe()); // #5 Archer (Sun) hp=80 atk=12 [volley]
The veteran's changes did not leak into the template, so the third archer is a plain archer again. Adding a new unit type is registering a new prototype; nothing that spawns units changes.
Prototype pattern vs JavaScript's prototype chain
The names collide, but these are different ideas:
- The Prototype pattern copies an object. The clone is independent; changing it does not affect the original.
- JavaScript's prototype chain delegates.
Object.create(base)makes a new object whose missing properties are looked up onbase. Nothing is copied.
const baseSettings = { theme: "dark", fontSize: 14, shortcuts: { save: "Ctrl+S" } };
const userSettings = Object.create(baseSettings) as typeof baseSettings;
userSettings.fontSize = 16; // own property, shadows the base
userSettings.shortcuts.save = "Cmd+S"; // no own `shortcuts`: this mutates the base object
console.log(baseSettings.fontSize, baseSettings.shortcuts.save); // 14 Cmd+S
Delegation is how JavaScript shares methods between instances of a class, and it is memory-efficient for that. For data that each object should own, it has exactly the shared-reference problem from the start of this chapter. Use structuredClone or a clone() method when you want independent copies.
Where you meet it
{ ...defaults, ...options }andstructuredClone(template)for configuration and request options.- Immutable updates in React and Redux: copy, then change the copy. Immer does this for you with structural sharing.
- Test data builders that start from a valid base object and override a field per test.
Array.prototype.slice(),new Map(existing),new Set(existing): shallow clones of collections.Response.clone()andRequest.clone()in the Fetch API, so a body can be read twice.
When to use it, and when not to
Use it when:
- Objects are configured from data (files, database, remote config) and you need many independent instances.
- Code needs to copy objects whose concrete type it does not know.
- Variants differ from a baseline by a few fields.
Skip it when:
- Objects are simple and cheap to build with a constructor or factory.
- Objects hold resources that cannot be meaningfully copied: sockets, file handles, database connections.
Common pitfalls
- Shallow copies of nested data. Spread,
Object.assignandslicecopy one level. Anything nested is shared. - Copying identity. Clones must get new IDs, timestamps and keys.
- Cloning class instances with
structuredCloneor JSON. You get plain objects without methods. Implementclone()on the class. - Circular and shared references in hand-written
clone(). If two units share oneSquadobject on purpose, a naive deep clone gives each copy its own squad. Decide per field.
Exercises
- Write a test that proves
Unit.clone()does not sharestatsorskillswith its prototype. - Add an
equipment: Map<string, { name: string; bonus: number }>field toUnitand updateclone()so equipment is deep-copied. - Change
UnitRegistryto load prototypes from a JSON file, and freeze each prototype with a deepObject.freezeso any accidental mutation throws in strict mode. - Compare
structuredClone,JSON.parse(JSON.stringify(x))and a hand-written clone on an object containing aDate, aMapand a circular reference. Which ones preserve each?