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