Prototype Pattern (GoF) — Прототип

Клонирование готового экземпляра вместо инициализации нового. В GoF — один из самых базовых паттернов, в JS — встроенное через structuredClone.

Проблема

Создание объекта дорогое: тяжёлая инициализация, обращение к БД, сложные вычисления. Если нужно много похожих объектов — выгоднее склонировать готовый.

Где используется

  • Клонирование конфигурационных объектов
  • Создание объектов на основе шаблона по умолчанию
  • Игровые объекты (NPC, предметы с похожими параметрами, тысячи врагов)
  • Иммутабельные обновления в Redux
  • HTMLElement.cloneNode(true) — клон DOM-узла

Решение

Делается эталонный объект. Все остальные создаются через клонирование, потом точечно модифицируются.

В JS есть несколько способов:

  • structuredClone(obj) — нативный глубокий клон (рекомендуемый, Node.js 17+)
  • JSON.parse(JSON.stringify(obj)) — старый трюк (не работает для функций, дат)
  • Object.create(proto) — наследование через прототип
  • Собственная функция deepClone

Реализации

structuredClone (рекомендуемый)

const prototype = {
  type: 'enemy',
  hp: 100,
  damage: 10,
  attack(target) { target.hp -= this.damage; },
};

// тысячи врагов: каждый — клон прототипа
const enemies = Array.from({ length: 1000 }, () => structuredClone(prototype));
enemies[0].hp = 50;

Object.create

const vehiclePrototype = {
  init(make, model) { this.make = make; this.model = model; return this; },
  getInfo { return `${this.make} ${this.model}`; },
  clone { return Object.create(this); }
};

const car = Object.create(vehiclePrototype).init('Toyota', 'Camry');
const clone = car.clone();
clone.make = 'Honda';
console.log(clone.getInfo); // 'Honda Camry'
console.log(car.getInfo);   // 'Toyota Camry'

Через метод clone в классе

class Shape {
  constructor(color, size) {
    this.color = color;
    this.size = size;
    this.metadata = { created: new Date };
  }
  clone {
    const c = Object.assign(Object.create(Object.getPrototypeOf(this)), this);
    c.metadata = { ...this.metadata }; // глубокий клон вложенного
    return c;
  }
  draw { return `${this.color} ${this.size}`; }
}

class Circle extends Shape {
  constructor(color, size, radius) { super(color, size); this.radius = radius; }
  clone {
    const c = super.clone();
    c.radius = this.radius;
    return c;
  }
}

const original = new Circle('red', 'large', 50);
const cloned = original.clone();
cloned instanceof Circle; // true

Реестр прототипов

class PrototypeRegistry {
  constructor { this._registry = {}; }
  register(name, proto) { this._registry[name] = proto; }
  create(name, overrides = {}) {
    const p = this._registry[name];
    if (!p) throw new Error(`'${name}' not registered`);
    return { ...p, ...overrides };
  }
}

const registry = new PrototypeRegistry();
registry.register('button', { type: 'button', disabled: false, className: 'btn' });
const primary = registry.create('button', { className: 'btn btn-primary' });

Где используется в JS-экосистеме

  • Геймдев: множество однотипных объектов (враги, частицы)
  • Redux: иммутабельные обновления { ...state, ...changes }
  • React: deep clone state перед мутацией
  • HTMLElement.cloneNode(true) — клон DOM-узла

Подводные камни

  • JSON.parse(JSON.stringify(x)) теряет: функции, Date, Map, Set, undefined, циклы.
  • structuredClone доступен в Node.js 17+ и современных браузерах — корректнее JSON.
  • Глубокий клон дорогой: для иммутабельных обновлений лучше shallow copy + новый объект.
  • Поверхностное клонирование: Object.assign и spread {...obj} копируют только первый уровень.
  • Потеря цепочки прототипов: JSON.parse(JSON.stringify(obj)) создаёт plain object без прототипа.
  • Не путать с прототипным наследованием JS — это разные вещи (хотя связь есть).
  • GoF Prototype vs JS prototype chain: GoF Prototype — паттерн клонирования; [[Prototype Pattern]] в JS — механизм наследования.

Главные тезисы автора

  • «Люди говорят, JS = прототипное программирование, паттерн не нужен. На самом деле нет.»
  • JSON.parse(JSON.stringify) — ближе к Prototype, чем к прототипной цепочке.
  • Prototype экономит память и вычисления — переиспользует структуру.
  • Прототипные цепочки JS ближе к Flyweight, а не к Prototype.

🎓 Источники

См. также