Полиморфизм
Полиморфизм — способность объектов разных типов отвечать на одно и то же сообщение (вызов метода) по-разному. В JavaScript реализуется через переопределение методов, duck typing и символы.
Зачем нужно
Полиморфизм позволяет писать код, который работает с объектами разных типов через единый интерфейс. Это уменьшает связанность, упрощает расширение системы и делает код универсальным.
Где используется
Обработка коллекций разнотипных объектов, стратегии, плагины, сериализация, рендеринг компонентов, middleware.
Предпосылки
Классы, Наследование, Прототипы
Переопределение методов (Override)
class Shape {
area {
throw new Error('Метод area должен быть реализован');
}
describe {
return `${this.constructor.name}: площадь = ${this.area.toFixed(2)}`;
}
}
class Circle extends Shape {
constructor(radius) {
super;
this.radius = radius;
}
area { return Math.PI * this.radius ** 2; }
}
class Rectangle extends Shape {
constructor(width, height) {
super;
this.width = width;
this.height = height;
}
area { return this.width * this.height; }
}
class Triangle extends Shape {
constructor(base, height) {
super;
this.base = base;
this.height = height;
}
area { return 0.5 * this.base * this.height; }
}
// Полиморфный код — работает с любой фигурой
const shapes = [
new Circle(5),
new Rectangle(4, 6),
new Triangle(3, 8)
];
shapes.forEach(shape => {
console.log(shape.describe);
});
// "Circle: площадь = 78.54"
// "Rectangle: площадь = 24.00"
// "Triangle: площадь = 12.00"
// Функция не знает конкретный тип
function totalArea(shapes) {
return shapes.reduce((sum, s) => sum + s.area, 0);
}
console.log(totalArea(shapes)); // 114.54
Duck Typing
В JavaScript не нужно формальное наследование — достаточно наличия нужных методов:
// "Если ходит как утка и крякает как утка — это утка"
class Logger {
log(message) { console.log(`[LOG] ${message}`); }
}
class FileWriter {
log(message) { /* запись в файл */ }
}
class ApiSender {
log(message) { /* отправка в API */ }
}
// Принимает любой объект с методом log
function processWithLogging(data, logger) {
logger.log(`Обработка: ${JSON.stringify(data)}`);
// ... обработка
logger.log('Завершено');
}
// Работает с любым "логгером"
processWithLogging({ id: 1 }, new Logger);
processWithLogging({ id: 2 }, new FileWriter);
processWithLogging({ id: 3 }, { log: (msg) => console.log(msg) }); // объект-литерал тоже
Symbol.toPrimitive
Управление преобразованием объекта в примитив:
class Money {
constructor(amount, currency) {
this.amount = amount;
this.currency = currency;
}
[Symbol.toPrimitive](hint) {
switch (hint) {
case 'number':
return this.amount;
case 'string':
return `${this.amount} ${this.currency}`;
default: // 'default'
return this.amount;
}
}
}
const price = new Money(100, 'USD');
console.log(`Цена: ${price}`); // "Цена: 100 USD" (string)
console.log(price + 50); // 150 (default → number)
console.log(+price); // 100 (number)
toString и valueOf
class Vector {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString {
return `Vector(${this.x}, ${this.y})`;
}
valueOf {
return Math.sqrt(this.x ** 2 + this.y ** 2); // длина вектора
}
}
const v = new Vector(3, 4);
console.log(`${v}`); // "Vector(3, 4)"
console.log(v + 0); // 5
console.log(v > 4); // true
Полиморфизм через интерфейсы (концепция)
JavaScript не имеет интерфейсов как язык, но можно создать проверку:
// Проверка "интерфейса" в runtime
function assertImplements(obj, interface_) {
for (const method of interface_) {
if (typeof obj[method] !== 'function') {
throw new TypeError(
`Объект не реализует метод ${method}`
);
}
}
}
const Serializable = ['serialize', 'deserialize'];
const Renderable = ['render', 'update'];
class Widget {
serialize { return JSON.stringify(this); }
deserialize(data) { return Object.assign(this, JSON.parse(data)); }
render { return `<div>${this.name}</div>`; }
update(props) { Object.assign(this, props); }
}
const widget = new Widget();
assertImplements(widget, Serializable); // OK
assertImplements(widget, Renderable); // OK
Symbol.iterator — полиморфная итерация
class Range {
constructor(start, end) {
this.start() = start;
this.end() = end;
}
[Symbol.iterator] {
let current = this.start();
const end = this.end();
return {
next {
return current <= end
? { value: current++, done: false }
: { done: true };
}
};
}
}
// Работает с for...of, spread, деструктуризацией
for (const n of new Range(1, 5)) {
console.log(n); // 1, 2, 3, 4, 5
}
console.log([...new Range(1, 3)]); // [1, 2, 3]
Паттерн Strategy как полиморфизм
class Sorter {
constructor(strategy) {
this.strategy = strategy;
}
sort(data) {
return this.strategy.sort([...data]);
}
}
const bubbleSort = {
sort(arr) {
for (let i = 0; i < arr.length; i++)
for (let j = 0; j < arr.length - i - 1; j++)
if (arr[j] > arr[j + 1])
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
return arr;
}
};
const quickSort = {
sort(arr) {
if (arr.length <= 1) return arr;
const pivot = arr[0];
const left = arr.slice(1).filter(x => x <= pivot);
const right = arr.slice(1).filter(x => x > pivot);
return [...this.sort(left), pivot, ...this.sort(right)];
}
};
const sorter = new Sorter(quickSort);
console.log(sorter.sort([3, 1, 4, 1, 5])); // [1, 1, 3, 4, 5]
sorter.strategy = bubbleSort; // смена стратегии
console.log(sorter.sort([3, 1, 4, 1, 5])); // [1, 1, 3, 4, 5]
Частые ошибки
1. Проверка типа вместо полиморфизма
// Плохо — switch по типу
function getArea(shape) {
switch (shape.type) {
case 'circle': return Math.PI * shape.radius ** 2;
case 'rect': return shape.width * shape.height;
// Каждый новый тип — новый case
}
}
// Хорошо — полиморфизм
function getArea(shape) {
return shape.area; // каждый объект знает, как считать
}
2. Забытый super при переопределении
class Child extends Parent {
doSomething {
// Забыли super.doSomething — логика родителя потеряна
this.childStuff;
}
}
3. Слишком жёсткая проверка типов
// Плохо — привязка к конкретному классу
if (logger instanceof ConsoleLogger) { ... }
// Хорошо — duck typing
if (typeof logger.log === 'function') { ... }
Практика
- Создай иерархию
Shapeс полиморфным методомareaиperimeter - Реализуй
Symbol.toPrimitiveдля классаCurrency - Напиши систему уведомлений с duck typing (email, sms, push — метод
send) - Реализуй
Symbol.iteratorдля классаLinkedList - Перепиши switch-based код на полиморфизм
Два вида полиморфизма
«Один полиморфизм — параметрический: функция принимает значения разных типов (дженерики в TypeScript, шаблоны в C++). Другой — полиморфизм подтипов (LSP), он же ООП-полиморфизм».
| Параметрический | дженерики, обобщённое программирование |
| Подтипов (LSP) | один интерфейс — разные реализации в наследниках |
В JS параметрический полиморфизм фактически бесплатен (нет статических типов). Полиморфизм подтипов реализуется через duck typing и переопределение.
Принцип подстановки Барбары Лисков (LSP)
«Вместо родительского класса можно передать любого потомка, и поведение должно остаться корректным. Вместо обычного сокета — веб-сокет с тем же интерфейсом».
Это работает и для классов, и для duck typing: если объект отвечает требуемому контракту (имеет нужные методы), он подходит.
Полиморфизм vs switch case
«Как только видим большой switch case по типу — задумываемся, может тут нужна Стратегия или другой полиморфный паттерн» (Antipatterns).
См. Антипаттерны ООП (Big switch case).
Мономорфизм/полиморфизм/мегаморфизм на уровне V8
Это другая разновидность полиморфизма — на уровне inline-кэшей V8:
- Mono — одно место обращения видело одну форму объекта (быстро)
- Poly — несколько форм (2-4) (средне)
- Mega — больше — V8 деоптимизирует (медленно)
Полиморфизм классов часто приводит к полиморфизму на уровне V8. Об этом подробно: автор, 9JUY3prnCQ4.
Связанные темы
Источники
- Timur · ООП: наследование и полиморфизм в JavaScript (2020-03-03)
- Timur · Object-oriented programming (2020-02-26)
- Timur · Мономорфный и полиморфный код, инлайн-кэш, скрытые классы (2019-10-29) — про V8
- Timur · Have Objects Failed? OOP critique (2019-05-23) — про два вида полиморфизма
- MDN — Symbol.toPrimitive
- JavaScript.info — Symbol.iterator