В JavaScript можно наследовать только от одного объекта. Объект имеет единственный [[Prototype]]. И класс может расширить только один другой класс.
Иногда это может ограничивать нас. Например, у нас есть класс StreetSweeper и класс Bicycle, а мы хотим создать их смесь: StreetSweepingBicycle.
Или у нас есть класс User, который реализует пользователей, и класс EventEmitter, реализующий события. Мы хотели бы добавить функциональность класса EventEmitter к User, чтобы пользователи могли легко генерировать события.
Для таких случаев существуют «примеси».
По определению из Википедии, примесь – это класс, методы которого предназначены для использования в других классах, причём без наследования от примеси.
Другими словами, примесь определяет методы, которые реализуют определённое поведение. Мы не используем примесь саму по себе, а используем её, чтобы добавить функциональность другим классам.
Пример примеси
Простейший способ реализовать примесь в JavaScript – это создать объект с полезными методами, которые затем могут быть легко добавлены в прототип любого класса.
В примере ниже примесь sayHiMixin имеет методы, которые придают объектам класса User возможность вести разговор:
// примесь
let sayHiMixin = {
sayHi() {
alert(`Привет, ${this.name}`);
},
sayBye() {
alert(`Пока, ${this.name}`);
}
};
// использование:
class User {
constructor(name) {
this.name = name;
}
}
// копируем методы
Object.assign(User.prototype, sayHiMixin);
// теперь User может сказать Привет
new User("Вася").sayHi(); // Привет, Вася!
Это не наследование, а просто копирование методов. Таким образом, класс User может наследовать от другого класса, но при этом также включать в себя примеси, «подмешивающие» другие методы, например:
class User extends Person {
// ...
}
Object.assign(User.prototype, sayHiMixin);
Примеси могут наследовать друг друга.
В примере ниже sayHiMixin наследует от sayMixin:
let sayMixin = {
say(phrase) {
alert(phrase);
}
};
let sayHiMixin = {
__proto__: sayMixin, // (или мы можем использовать Object.setPrototypeOf для задания прототипа)
sayHi() {
// вызываем метод родителя
super.say(`Привет, ${this.name}`); // (*)
},
sayBye() {
super.say(`Пока, ${this.name}`); // (*)
}
};
class User {
constructor(name) {
this.name = name;
}
}
// копируем методы
Object.assign(User.prototype, sayHiMixin);
// теперь User может сказать Привет
new User("Вася").sayHi(); // Привет, Вася!
Обратим внимание, что при вызове родительского метода super.say() из sayHiMixin (строки, помеченные (*)) этот метод ищется в прототипе самой примеси, а не класса.
Вот диаграмма (см. правую часть):
Это связано с тем, что методы sayHi и sayBye были изначально созданы в объекте sayHiMixin. Несмотря на то, что они скопированы, их внутреннее свойство [[HomeObject]] ссылается на sayHiMixin, как показано на картинке выше.
Так как super ищет родительские методы в [[HomeObject]].[[Prototype]], это означает, что он ищет sayHiMixin.[[Prototype]].
EventMixin
Многие объекты в браузерной разработке (и не только) обладают важной способностью – они могут генерировать события. События – отличный способ передачи информации всем, кто в ней заинтересован. Давайте создадим примесь, которая позволит легко добавлять функциональность по работе с событиями любым классам/объектам.
- Примесь добавит метод
.trigger(name, [...data])для генерации события. Аргументname– это имя события, за которым могут следовать дополнительные аргументы с данными для события. - Также будет добавлен метод
.on(name, handler), который назначает обработчик для события с заданным именем. Обработчик будет вызван, когда произойдёт событие с указанным именемname, и получит данные из.trigger. - …и метод
.off(name, handler), который удаляет обработчик указанного события.
После того, как все методы примеси будут добавлены, объект user сможет сгенерировать событие "login" после входа пользователя в личный кабинет. А другой объект, к примеру, calendar сможет использовать это событие, чтобы показывать зашедшему пользователю актуальный для него календарь.
Или menu может генерировать событие "select", когда элемент меню выбран, а другие объекты могут назначать обработчики, чтобы реагировать на это событие, и т.п.
Вот код примеси:
let eventMixin = {
/**
* Подписаться на событие, использование:
* menu.on('select', function(item) { ... }
*/
on(eventName, handler) {
if (!this._eventHandlers) this._eventHandlers = {};
if (!this._eventHandlers[eventName]) {
this._eventHandlers[eventName] = [];
}
this._eventHandlers[eventName].push(handler);
},
/**
* Отменить подписку, использование:
* menu.off('select', handler)
*/
off(eventName, handler) {
let handlers = this._eventHandlers?.[eventName];
if (!handlers) return;
for (let i = 0; i < handlers.length; i++) {
if (handlers[i] === handler) {
handlers.splice(i--, 1);
}
}
},
/**
* Сгенерировать событие с указанным именем и данными
* this.trigger('select', data1, data2);
*/
trigger(eventName, ...args) {
if (!this._eventHandlers?.[eventName]) {
return; // обработчиков для этого события нет
}
// вызовем обработчики
this._eventHandlers[eventName].forEach(handler => handler.apply(this, args));
}
};
Итак, у нас есть 3 метода:
-
.on(eventName, handler)– назначает функциюhandler, чтобы обработать событие с заданным именем.Технически существует свойство
_eventHandlers, в котором хранится массив обработчиков для каждого имени события, и оно просто добавляет это событие в список. -
.off(eventName, handler)– убирает функцию из списка обработчиков. -
.trigger(eventName, ...args)– генерирует событие: все назначенные обработчики из_eventHandlers[eventName]вызываются, и...argsпередаются им в качестве аргументов.
Использование:
// Создадим класс
class Menu {
choose(value) {
this.trigger("select", value);
}
}
// Добавим примесь с методами для событий
Object.assign(Menu.prototype, eventMixin);
let menu = new Menu();
// Добавим обработчик, который будет вызван при событии "select":
menu.on("select", value => alert(`Выбранное значение: ${value}`));
// Генерирует событие => обработчик выше запускается и выводит:
menu.choose("123"); // Выбранное значение: 123
Теперь если у нас есть код, заинтересованный в событии "select", то он может слушать его с помощью menu.on(...).
А eventMixin позволяет легко добавить такое поведение в любой класс без вмешательства в цепочку наследования.
Итого
Примесь – общий термин в объектно-ориентированном программировании: класс, который содержит в себе методы для других классов.
Некоторые другие языки допускают множественное наследование. JavaScript не поддерживает множественное наследование, но с помощью примесей мы можем реализовать нечто похожее, скопировав методы в прототип.
Мы можем использовать примеси для расширения функциональности классов, например, для обработки событий, как мы сделали это выше.
С примесями могут возникнуть конфликты, если они перезаписывают существующие методы класса. Стоит помнить об этом и быть внимательнее при выборе имён для методов примеси, чтобы их избежать.
Комментарии
<code>, для нескольких строк кода — тег<pre>, если больше 10 строк — ссылку на песочницу (plnkr, JSBin, codepen…)