---
title: Prototype
source: 'https://academia.sh/en/courses/design-patterns/prototype'
course: 'Design Patterns'
language: en
updated: '2026-08-23T07:01:14+00:00'
license: 'CC BY-SA 4.0'
---

# Prototype

Creating an object by copying an existing one instead of constructing it: comparing a version that hand-writes derived tariffs' fields against one that clones a prototype, measuring the missing-field count when a field is added to the prototype, counting the child objects a shallow copy shares, and the cost of the derivation chain returning as a silent change.

The builder constructs every object from nothing. Most objects built in the library are small
departures from each other: a contracted customer's tariff is the standard tariff with two
fields changed, a zonal tariff the same with one zone factor changed, a corporate tariff a
change to one field of the contracted tariff. Building these step by step means writing the
same fields over and over.

The **prototype** pattern turns creation into copying: a new object is obtained by copying an
existing object and changing a few fields on it. This lesson has two measures. The first is the
gain: when a field is added to the prototype, how many fields go missing in the derived objects.
The second is the cost: how many objects an unwanted change to a shared reference reaches, and
how many child objects the copy shares with the prototype.

## Hand-Written Derived Tariffs

In the first version, all four tariffs are independent. Each carries its five fields written out
in its own text.

```js
// manual/tariff.mjs — every derived tariff rewrites all of its fields
export const standard = {
  name: "standard",
  tiers: [[1, 3900], [5, 6400], [20, 11800]],
  zone: { "34": 100, "06": 115, "65": 140 },
  minimumFee: 3900,
  insuranceRate: 0.004,
};

export const contracted = {
  name: "contracted",
  tiers: [[1, 3900], [5, 6400], [20, 11800]],
  zone: { "34": 100, "06": 115, "65": 140 },
  minimumFee: 3200,
  insuranceRate: 0.003,
};

export const zonal = {
  name: "zonal",
  tiers: [[1, 3900], [5, 6400], [20, 11800]],
  zone: { "34": 100, "06": 115, "65": 125 },
  minimumFee: 3900,
  insuranceRate: 0.004,
};

export const corporate = {
  name: "corporate",
  tiers: [[1, 3900], [5, 6400], [20, 11800]],
  zone: { "34": 100, "06": 115, "65": 140 },
  minimumFee: 3200,
  insuranceRate: 0.002,
};
```

The tier table is written four times, the zone table three, in identical form. This is the
knowledge duplication defined in the Clean Code course: nothing guarantees the four objects'
tier tables stay equal.

## Cloning from a Prototype

In the second version, only the standard tariff is written in full. The others come from a
`derive` call: the prototype is cloned, its name and the deriving prototype's name are written
on, and the requested fields are then applied over the top.

```js
// prototype/tariff.mjs — derived tariffs clone the prototype and change fields
export const standard = {
  name: "standard",
  tiers: [[1, 3900], [5, 6400], [20, 11800]],
  zone: { "34": 100, "06": 115, "65": 140 },
  minimumFee: 3900,
  insuranceRate: 0.004,
};

export function derive(prototype, name, changes) {
  const copy = structuredClone(prototype);
  return Object.assign(copy, { name, derivedFrom: prototype.name }, changes);
}

export const contracted = derive(standard, "contracted", { minimumFee: 3200, insuranceRate: 0.003 });
export const zonal = derive(standard, "zonal", { zone: { ...standard.zone, "65": 125 } });
export const corporate = derive(contracted, "corporate", { insuranceRate: 0.002 });
```

The last line shows what sets the pattern apart: the corporate tariff derives not from the
standard tariff but from **the contracted tariff**. A prototype is not a class but a running
object, so a derived object can itself be a prototype. This mechanism has nothing to do with
JavaScript's own `[[Prototype]]` link, covered in the Objects and Functions in JavaScript
course; that link builds a lookup chain, while the clone here is an independent object carrying
its fields as copies.

The following script computes the fee for the same shipment under all four tariffs in a given
directory. This is how the two versions' equivalence is checked.

```js
// price.mjs — the fee for the same shipment under the four tariffs in the given directory
const dir = process.argv[2];
const t = await import(`./${dir}/tariff.mjs`);
const S = { weight: 2.4, province: "65", declaredValue: 180000 };

const fee = (tf) => {
  const baseFee = tf.tiers.find(([max]) => S.weight <= max)?.[1] ?? 15000;
  const raw = Math.round((baseFee * (tf.zone[S.province] ?? 160)) / 100) +
    Math.round(S.declaredValue * tf.insuranceRate);
  return Math.max(raw, tf.minimumFee);
};

for (const name of ["standard", "contracted", "zonal", "corporate"])
  console.log(`${dir.padEnd(9)} ${name.padEnd(11)} ${fee(t[name])} cents`);
```

```sh
node price.mjs manual
node price.mjs prototype
```

```
manual    standard    9680 cents
manual    contracted  9500 cents
manual    zonal       8720 cents
manual    corporate   9320 cents
prototype standard    9680 cents
prototype contracted  9500 cents
prototype zonal       8720 cents
prototype corporate   9320 cents
```

All four fees match. The only difference is how the derived objects were obtained.

## Shallow Copy Versus Deep Copy

The pattern's first trap is the copy's depth. Object spread (`{ ...prototype }`) copies fields
one by one, but when a field points to an object or array, what gets copied is the reference
itself. The measurer below counts reference equality for the prototype's two child objects, then
changes a tier value on each copy and reports what happened to the prototype.

```js
// sharing.mjs — child objects the shallow and deep copies share with the prototype
import { standard } from "./prototype/tariff.mjs";

const CHILDREN = ["tiers", "zone"];
const shared = (t) => CHILDREN.filter((c) => t[c] === standard[c]).length;

const shallow = { ...standard, name: "shallow-copy" };
const deep = structuredClone(standard);
deep.name = "deep-copy";

console.log(`shallow shared-child-objects=${shared(shallow)}`);
console.log(`deep    shared-child-objects=${shared(deep)}`);

deep.tiers[0][1] = 4200;
console.log(`deep tier changed -> standard first tier=${standard.tiers[0][1]}`);
shallow.tiers[0][1] = 4200;
console.log(`shallow tier changed -> standard first tier=${standard.tiers[0][1]}`);
```

```sh
node sharing.mjs
```

```
shallow shared-child-objects=2
deep    shared-child-objects=0
deep tier changed -> standard first tier=3900
shallow tier changed -> standard first tier=4200
```

The shallow copy shares both child objects with the prototype. The result shows in the last
line: writing to the shallow copy's tier also changed the prototype's tier. This is the same
common coupling measured in the Types of Coupling lesson — two objects' behavior tied to one
shared mutable state — and it is the most common way the pattern gets applied wrong. In the deep
copy, the shared child object count is zero, so the prototype did not change.

## Tracking a Field Added to the Prototype

The pattern's gain shows up when the prototype changes. The measurer below counts, across the
three derived tariffs, how many fields stay undefined, lists their insurance rates, and gives the
depth of the derivation chain.

```js
// field-count.mjs — missing field, insurance rate and prototype chain of the derived tariffs
const dir = process.argv[2];
const t = await import(`./${dir}/tariff.mjs`);
const FIELDS = ["name", "tiers", "zone", "minimumFee", "insuranceRate", "fuelSurcharge"];
const DERIVED = ["contracted", "zonal", "corporate"];

const missing = DERIVED.reduce(
  (total, name) => total + FIELDS.filter((f) => t[name][f] === undefined).length, 0);
const rate = DERIVED.map((name) => t[name].insuranceRate).join(",");
const chain = (name) => (t[name].derivedFrom === undefined ? 0 : 1 + chain(t[name].derivedFrom));
const depth = Math.max(...DERIVED.map(chain));

console.log(`${dir.padEnd(13)} missing-field=${missing} insurance-rate=${rate} ` +
  `chain-depth=${depth}`);
```

The change is a single request: add a fuel surcharge field to the standard tariff and raise its
insurance rate. The script below applies that edit, in identical form, to copies of both trees;
it locates the text by name to make sure it only touches the standard tariff's field.

```js
// patch.mjs — raises the standard tariff's insurance rate and adds a fuel surcharge field
import { readFileSync, writeFileSync } from "node:fs";

const path = process.argv[2];
const text = readFileSync(path, "utf8");
const start = text.indexOf('name: "standard"');
const field = text.indexOf("insuranceRate:", start);
const lineEnd = text.indexOf("\n", field);
writeFileSync(path,
  `${text.slice(0, field)}insuranceRate: 0.006,\n  fuelSurcharge: 250,${text.slice(lineEnd)}`);
```

```sh
node field-count.mjs manual
node field-count.mjs prototype
cp -r manual manual-new
cp -r prototype prototype-new
node patch.mjs manual-new/tariff.mjs
node patch.mjs prototype-new/tariff.mjs
node field-count.mjs manual-new
node field-count.mjs prototype-new
node price.mjs manual-new
node price.mjs prototype-new
```

```
manual        missing-field=3 insurance-rate=0.003,0.004,0.002 chain-depth=0
prototype     missing-field=3 insurance-rate=0.003,0.004,0.002 chain-depth=2
manual-new    missing-field=3 insurance-rate=0.003,0.004,0.002 chain-depth=0
prototype-new missing-field=0 insurance-rate=0.003,0.006,0.002 chain-depth=2
manual-new standard    10040 cents
manual-new contracted  9500 cents
manual-new zonal       8720 cents
manual-new corporate   9320 cents
prototype-new standard    10040 cents
prototype-new contracted  9500 cents
prototype-new zonal       9080 cents
prototype-new corporate   9320 cents
```

The gain is in the first column. Before the edit, both versions had three missing fields; the
fuel surcharge was absent from every tariff. After the edit, the hand-written count stayed at
3: the new field only reached the standard tariff, and the derived ones never saw it. In the
cloning version, the count fell to 0, because the derived tariffs are produced from the
prototype at load time.

The cost is in the second and third columns. The insurance rate did not change for the
contracted and corporate tariffs, already overwritten on each; it had not been overwritten on
the zonal tariff, and rose from 0.004 to 0.006. The fee lines show the counterpart: the zonal
tariff's fee stayed at 8720 cents in the hand-written version and rose to 9080 cents in the
cloning version. Nobody asked for this change anywhere; only the prototype was touched.

The third column says why this cost is hard to notice. The chain's depth is 2: the corporate
tariff derives from the contracted tariff, which derives from the standard tariff. Finding a
field's final value means walking the chain backward, checking at each step whether it was
overwritten. In the hand-written version, this depth is 0: every tariff's fields live entirely
in its own text.

The two numbers are two faces of the same bond: a new field carrying over on its own and an
unwanted change carrying over on its own. The pattern is applied where derived objects
genuinely should change along with the prototype; where they must change independently, a
carried-over change is a defect, not a gain.

## Summary

- The prototype pattern produces a new object not through construction steps but from a copy of
  an existing object; a prototype is a running object, not a class, so a derived object can
  itself be a prototype.
- When a field was added to the prototype, the hand-written version's missing-field count stayed
  at 3, and the cloning version's fell to 0.
- The same edit moved the zonal tariff's insurance rate from 0.004 to 0.006 and its fee from
  8720 cents to 9080 cents; that change had not been requested.
- The shallow copy shared 2 child objects with the prototype, and writing to the copy's tier also
  changed the prototype's; the deep copy's shared child object count is 0.
- The derivation chain's depth is 2 in the cloning version and 0 in the hand-written version; a
  field's final value can only be found by walking the chain.

## Next Step

All four creational mechanisms arranged where the object comes from, but in every one, the
object was held by the caller itself. The next question is **how many** of the object there
are. The library has objects meant to exist as a single instance, such as the tariff registry;
the shortest way to satisfy that requirement is to build the object at the module level and
reach the same instance from everywhere. The next lesson measures that requirement's cost in
testability: how many tests fail when the same test set runs against a single shared instance,
whether the result changes when the order changes, and how many names the reset mechanism adds
to the surface it exposes.
