Flyweight Pattern — Приспособленец
Экономия памяти за счёт разделения общего состояния между множеством объектов. В JS реализуется естественно через прототипы.
Проблема
Нужно тысячи однотипных объектов: враги в игре, частицы, иконки, символы текста, маркеры на карте. 90% полей у них одинаковые. Хранить каждый отдельно — миллионы байт в памяти.
Где используется
- Текстовые редакторы: символы шрифта как Flyweight
- Частицы в играх: тысячи пуль/звёздочек с общей геометрией и текстурой
- Пины на карте: маркеры с общим SVG-иконкой, уникальные координаты
- DOM-события через делегирование: один обработчик для тысяч элементов
- Кэш объектов:
Symbol.for— глобальный Flyweight для символов
Решение
Два состояния:
- Внутреннее (intrinsic) — общее, разделяемое. Хранится в эталоне.
- Внешнее (extrinsic) — уникальное. Хранится в инстансе.
В JS можно через прототипы: один класс, эталонный объект как прототип.
Реализации
Классический GoF (Tree Factory)
class TreeType {
constructor(name, color, texture) {
this.name = name;
this.color = color;
this.texture = texture; // большой объект текстуры
}
draw(canvas, x, y, scale) { canvas.drawImage(this.texture, x, y, scale, scale); }
}
class TreeTypeFactory {
static #cache = new Map();
static getTreeType(name, color, texture) {
const key = `${name}_${color}`;
if (!this.#cache.has(key)) this.#cache.set(key, new TreeType(name, color, texture));
return this.#cache.get(key);
}
static getCount { return this.#cache.size; }
}
class Tree {
constructor(x, y, scale, type) {
this.x = x; this.y = y; this.scale = scale;
this.type = type; // ссылка на разделяемый TreeType
}
draw(canvas) { this.type.draw(canvas, this.x, this.y, this.scale); }
}
// 10 000 деревьев, но только 3 TreeType в памяти
const trees = ;
for (let i = 0; i < 10000; i++) {
const types = ['дуб', 'берёза', 'ель'];
const typeName = types[i % 3];
const type = TreeTypeFactory.getTreeType(typeName, '#2d5a27', textures[typeName]);
trees.push(new Tree(Math.random * 800, Math.random * 600, 1, type));
}
TreeTypeFactory.getCount; // 3
JS-вариант через прототипы (один класс)
const cache = new Map();
class Interval {
constructor(msec, callback) {
this.callback = callback; // уникальное — в инстансе
let timer = cache.get(msec);
if (!timer) {
timer = makeTimer(msec); // эталон создаётся один раз
cache.set(msec, timer);
}
Object.setPrototypeOf(this, timer); // общее — через прототип
}
remove {
const timer = Object.getPrototypeOf(this);
timer.listeners.delete(this.callback);
}
}
// Тысячи Interval(1000, ...) делят один timer
Через Map (функциональный)
const markerPool = new Map();
function getMarkerIcon(type) {
if (!markerPool.has(type)) markerPool.set(type, createSVGIcon(type));
return markerPool.get(type);
}
const markers = locations.map(loc => ({
lat: loc.lat,
lng: loc.lng,
icon: getMarkerIcon(loc.type)
}));
DOM-делегирование как Flyweight
// Один обработчик для тысяч элементов — extrinsic state в data-атрибутах
list.addEventListener('click', (e) => {
const item = e.target.closest('.item-button');
if (!item) return;
handleItemClick(item.dataset.id);
});
Где используется в JS-экосистеме
- V8 hidden classes — внутренний flyweight для объектов одной формы
- Геймдев: тысячи врагов с общим прототипом (HP, damage, animations)
- Symbol.for(str) — глобальный реестр символов = flyweight
- String interning — браузеры дедуплицируют строки
- React reconciliation: ключи и общие props между элементами
Подводные камни
- Flyweight vs Prototype: Prototype клонирует целиком; Flyweight шарит общее и хранит уникальное отдельно.
- Flyweight vs Singleton: Singleton — один объект на процесс; Flyweight — много объектов с общим состоянием.
- Мутации эталона ломают всех — общее состояние должно быть read-only.
- В JS легко напортачить с прототипами:
setPrototypeOfмедленный, в hot path лучше избегать. - Преждевременная оптимизация: Flyweight усложняет код. Применяй только если профилирование показало проблему с памятью.
- Путаница с Proxy и Boxing/Container — общее: за абстракцией стоит другая.
Главные тезисы автора
- «Нативный для JS способ — через прототипное наследование с одним классом».
- «Часть свойств в лёгком классе, часть — в общем».
- «Прототипные цепочки JS ближе всего к Flyweight» (не к Prototype-паттерну).
- «Это реально JS way: паттерн на одном классе вместо трёх».
- Критерий паттерна — переиспользуемость: процедурный однострочник для таймеров — это не Flyweight.
🎓 Источники
- 🎓 Паттерн Flyweight (GoF) из курса Patterns · 2024-08-02
- Flyweight на прототипах в одном классе
- Классический способ — три класса; JS — один
- Экономия на коллекции listeners и instance setInterval
- 🎓 GoF Patterns Обзор всех паттернов · 2025-04-29
- Геймдев как канонический пример
- 🎓 Адаптер, Декоратор, Прокси, Фасад — В чём разница · 2026-02-09
- refactoring.guru — Flyweight