// ───────────────────────────────────────────────────────────────────────────── // PROTOTYPE [Creational] // ───────────────────────────────────────────────────────────────────────────── // INTENT : Specify the kinds of objects to create using a prototypical instance, and create new objects // by copying this prototype. // PROBLEM : Building an object from scratch is expensive or needs runtime-only configuration (a template // tuned per region/tenant); you'd rather copy a pre-made one and tweak it. // SAY : Participants: Prototype (declares clone()), ConcretePrototype (implements deep/shallow copy), // Client (calls clone() instead of `new`). Pitch: "copy a configured instance instead of // constructing one." // SEEN IN : JavaScript itself — prototypal inheritance, `Object.create(proto)`; `structuredClone()`; // `{ ...obj }` / `[...arr]` copies in Redux reducers & React setState; `new Map(map)`, // `Buffer.from(buf)`, `Request.clone()` / `Response.clone()` in fetch; document/email templates // cloned per recipient; Immer `produce` (copy-on-write). // VS : Factory Method creates fresh via `new`; Prototype copies an existing instance. Builder assembles // step by step; Prototype starts from a finished object. Memento snapshots state to RESTORE the // same object; Prototype copies to make a NEW one. // ───────────────────────────────────────────────────────────────────────────── 'use strict'; const assert = require('node:assert/strict'); // ── Classic shape (classes, so the participant names are visible) class InvoiceTemplate { // Prototype + ConcretePrototype constructor({ currency = 'USD', taxRate = 0, lines = [], footer = '' } = {}) { Object.assign(this, { currency, taxRate, lines, footer }); } addLine(sku, cents, qty = 1) { this.lines.push({ sku, cents, qty }); return this; } total() { const sub = this.lines.reduce((s, l) => s + l.cents * l.qty, 0); return Math.round(sub * (1 + this.taxRate)); } clone() { // deep-copy mutable parts, keep the class return new InvoiceTemplate({ currency: this.currency, taxRate: this.taxRate, lines: this.lines.map((l) => ({ ...l })), footer: this.footer, }); } } // Client: one configured prototype per region, cloned per customer const euTemplate = new InvoiceTemplate({ currency: 'EUR', taxRate: 0.2, footer: 'VAT included' }) .addLine('SUPPORT', 10000); const inv1 = euTemplate.clone().addLine('SEAT', 2500, 4); const inv2 = euTemplate.clone(); assert.ok(inv1 instanceof InvoiceTemplate); // class preserved assert.notEqual(inv1, euTemplate); // different identity assert.equal(inv1.total(), Math.round((10000 + 10000) * 1.2)); assert.equal(inv2.total(), 12000); // prototype untouched by inv1's extra line assert.equal(euTemplate.lines.length, 1); inv2.lines[0].cents = 1; // deep copy: clone's line is not the template's assert.equal(euTemplate.lines[0].cents, 10000); // ── Idiomatic JS shape: Object.create, structuredClone, spread // 1) Object.create — the new object DELEGATES to the prototype (shares behavior, overrides data) const baseConfig = { retries: 3, timeoutMs: 5000, describe() { return `${this.retries}x/${this.timeoutMs}ms`; }, }; const slowConfig = Object.create(baseConfig, { timeoutMs: { value: 30000, enumerable: true } }); assert.equal(slowConfig.describe(), '3x/30000ms'); // own timeoutMs, inherited retries + describe assert.equal(Object.getPrototypeOf(slowConfig), baseConfig); baseConfig.retries = 5; assert.equal(slowConfig.retries, 5); // live link, not a copy — know which one you want // 2) structuredClone — real deep copy of DATA (Date, Map, Set, nested arrays); drops functions/prototypes const orderPreset = { status: 'draft', items: [{ sku: 'A', qty: 1 }], meta: new Map([['src', 'web']]) }; const order = structuredClone(orderPreset); order.items.push({ sku: 'B', qty: 2 }); order.meta.set('src', 'api'); assert.equal(orderPreset.items.length, 1); assert.equal(orderPreset.meta.get('src'), 'web'); assert.ok(order.meta instanceof Map); assert.throws(() => structuredClone({ fn() {} }), /could not be cloned/); assert.equal(Object.getPrototypeOf(structuredClone(euTemplate)), Object.prototype); // class lost! // 3) Spread — SHALLOW copy; the everyday "clone then tweak" in reducers const shallow = { ...orderPreset, status: 'paid' }; assert.equal(shallow.status, 'paid'); assert.equal(shallow.items, orderPreset.items); // nested array is shared — classic footgun console.log('ok: design-patterns/creational/prototype.js');