Примеси

В JavaScript невозможно унаследовать от двух и более объектов. Ссылка __proto__ – только одна.

Но потребность такая существует – к примеру, мы написали код, реализующий методы работы с шаблонизатором или методы по обмену событиями, и хочется легко и непринуждённо добавлять эти возможности к любому классу.

Обычно это делают через примеси.

Примесь (англ. mixin) – класс или объект, реализующий какое-либо чётко выделенное поведение. Используется для уточнения поведения других классов, не предназначен для самостоятельного использования.

Пример примеси

Самый простой вариант примеси – это объект с полезными методами, которые мы просто копируем в нужный прототип.

Например:

// примесь
var sayHiMixin = {
  sayHi: function() {
    alert("Привет " + this.name);
  },
  sayBye: function() {
    alert("Пока " + this.name);
  }
};

// использование:
function User(name) {
  this.name = name;
}

// передать методы примеси
for(var key in sayHiMixin) User.prototype[key] = sayHiMixin[key];

// User "умеет" sayHi
new User("Вася").sayHi(); // Привет Вася

Как видно из примера, методы примеси активно используют this и предназначены именно для запуска в контексте «объекта-носителя примеси».

Если какие-то из методов примеси не нужны – их можно перезаписать своими после копирования.

Примесь для событий

Теперь пример из реальной жизни.

Важный аспект, который может понадобиться объектам – это умение работать с событиями.

То есть, чтобы объект мог специальным вызовом генерировать «уведомление о событии», а на эти уведомления другие объекты могли «подписываться», чтобы их получать.

Например, объект «Пользователь» при входе на сайт может генерировать событие "login", а другие объекты, например «Календарь» может такие уведомления получать и подгружать информацию о пользователе.

Или объект «Меню» может при выборе пункта меню генерировать событие "select" с информацией о выбранном пункте меню, а другие объекты – подписавшись на это событие, будут узнавать об этом.

События – это средство «поделиться информацией» с неопределённым кругом заинтересованных лиц. А там уже кому надо – тот среагирует.

Примесь eventMixin, реализующая события:

var eventMixin = {

  /**
   * Подписка на событие
   * Использование:
   *  menu.on('select', function(item) { ... }
   */
  on: function(eventName, handler) {
    if (!this._eventHandlers) this._eventHandlers = {};
    if (!this._eventHandlers[eventName]) {
      this._eventHandlers[eventName] = [];
    }
    this._eventHandlers[eventName].push(handler);
  },

  /**
   * Прекращение подписки
   *  menu.off('select',  handler)
   */
  off: function(eventName, handler) {
    var handlers = this._eventHandlers && this._eventHandlers[eventName];
    if (!handlers) return;
    for(var i=0; i<handlers.length; i++) {
      if (handlers[i] == handler) {
        handlers.splice(i--, 1);
      }
    }
  },

  /**
   * Генерация события с передачей данных
   *  this.trigger('select', item);
   */
  trigger: function(eventName /*, ... */) {

    if (!this._eventHandlers || !this._eventHandlers[eventName]) {
      return; // обработчиков для события нет
    }

    // вызвать обработчики
    var handlers = this._eventHandlers[eventName];
    for (var i = 0; i < handlers.length; i++) {
      handlers[i].apply(this, [].slice.call(arguments, 1));
    }

  }
};

Здесь есть три метода:

  1. .on(имя события, функция) – назначает функцию к выполнению при наступлении события с данным именем. Такие функции хранятся в защищённом свойстве объекта _eventHandlers.
  2. .off(имя события, функция) – удаляет функцию из списка предназначенных к выполнению.
  3. .trigger(имя события, аргументы) – генерирует событие, при этом вызываются все назначенные на него функции, и им передаются аргументы.

Использование:

// Класс Menu с примесью eventMixin
function Menu() {
  // ...
}

for(var key in eventMixin) {
  Menu.prototype[key] = eventMixin[key];
}

// Генерирует событие select при выборе значения
Menu.prototype.choose = function(value) {
  this.trigger("select", value);
}

// Создадим меню
var menu = new Menu();

// При наступлении события select вызвать эту функцию
menu.on("select", function(value) {
  alert("Выбрано значение " + value);
});

// Запускаем выбор (событие select вызовет обработчики)
menu.choose("123");

…То есть, смысл событий – обычно в том, что объект, в процессе своей деятельности, внутри себя (this.trigger) генерирует уведомления, на которые внешний код через menu.on(...) может быть подписан. И узнавать из них ценную информацию о происходящем, например – что выбран некий пункт меню.

Один раз написав методы on/off/trigger в примеси, мы затем можем использовать их во множестве прототипов.

Итого

  • Примесь – объект, содержащий методы и свойства для реализации конкретного функционала. Возможны вариации этого приёма проектирования. Например, примесь может предусматривать конструктор, который должен запускаться в конструкторе объекта. Но как правило просто набора методов хватает.
  • Для добавления примеси в класс – её просто «подмешивают» в прототип.
  • «Подмешать» можно сколько угодно примесей, но если имена методов в разных примесях совпадают, то возможны конфликты. Их уже разрешать – разработчику. Например, можно заменить конфликтующий метод на свой, который будет решать несколько задач сразу. Конфликты при грамотно оформленных примесях возникают редко.
Карта учебника

Комментарии

перед тем как писать…
  • Приветствуются комментарии, содержащие дополнения и вопросы по статье, и ответы на них.
  • Для одной строки кода используйте тег <code>, для нескольких строк кода — тег <pre>, если больше 10 строк — ссылку на песочницу (plnkr, JSBin, codepen…)
  • Если что-то непонятно в статье — пишите, что именно и с какого места.