Em JavaScript só podemos herdar de um único objeto. Só pode haver um [[Prototype]]
para um objeto. E uma classe pode estender apenas uma outra classe.
Mas às vezes isso parece limitante. Por exemplo, temos uma classe StreetSweeper
e uma classe Bicycle
, e queremos fazer a mistura deles: a StreetSweepingBicycle
.
Ou temos uma classe User
e uma classe EventEmitter
que implementa a geração de eventos e gostaríamos de adicionar a funcionalidade de EventEmitter
a User
, para que nossos usuários possam emitir eventos.
Existe um conceito que pode ajudar aqui, chamado “mixins”.
Conforme definido na Wikipedia, um mixin é uma classe que contém métodos que podem ser usados por outras classes sem a necessidade de herdar dela.
Ou seja, um mixin fornece métodos que implementam um determinado comportamento, mas não o utilizamos sozinho, utilizamos para adicionar o comportamento a outras classes.
A maneira mais simples de implementar um mixin em JavaScript é criar um objeto com métodos úteis, para que possamos mesclá-los facilmente em um protótipo de qualquer classe.
Por exemplo, aqui o mixin sayHiMixin
é usado para adicionar alguma “fala” para User
:
// mixando digamosHiMixin = { digaOi() { alert(`Olá ${this.name}`); }, digaTchau() { alert(`Tchau ${this.name}`); } }; // uso: classe Usuário { construtor(nome) { este.nome = nome; } } //copia os métodos Object.assign(User.prototype, sayHiMixin); // agora o usuário pode dizer oi novo usuário("Cara").sayHi(); // Olá cara!
Não há herança, mas sim um método simples de cópia. Assim, User
pode herdar de outra classe e também incluir o mixin para “combinar” os métodos adicionais, como este:
classe Usuário estende Pessoa { // ... } Object.assign(User.prototype, sayHiMixin);
Os mixins podem fazer uso da herança dentro de si.
Por exemplo, aqui sayHiMixin
herda de sayMixin
:
digamosMixin = { dizer(frase) { alerta(frase); } }; digamosHiMixin = { __proto__: sayMixin, // (ou poderíamos usar Object.setPrototypeOf para definir o protótipo aqui) digaOi() { //chama o método pai super.say(`Olá ${this.name}`); // (*) }, digaTchau() { super.say(`Tchau ${this.name}`); // (*) } }; classe Usuário { construtor(nome) { este.nome = nome; } } //copia os métodos Object.assign(User.prototype, sayHiMixin); // agora o usuário pode dizer oi novo usuário("Cara").sayHi(); // Olá cara!
Observe que a chamada para o método pai super.say()
de sayHiMixin
(nas linhas marcadas com (*)
) procura o método no protótipo desse mixin, não na classe.
Aqui está o diagrama (veja a parte direita):
Isso porque os métodos sayHi
e sayBye
foram criados inicialmente em sayHiMixin
. Portanto, mesmo que tenham sido copiados, suas referências de propriedade interna [[HomeObject]]
sayHiMixin
, conforme mostrado na imagem acima.
Como super
procura métodos pai em [[HomeObject]].[[Prototype]]
, isso significa que ele pesquisa sayHiMixin.[[Prototype]]
.
Agora vamos fazer um mixin para a vida real.
Uma característica importante de muitos objetos de navegador (por exemplo) é que eles podem gerar eventos. Os eventos são uma ótima maneira de “transmitir informações” para quem quiser. Então, vamos fazer um mixin que nos permita adicionar facilmente funções relacionadas a eventos a qualquer classe/objeto.
O mixin fornecerá um método .trigger(name, [...data])
para “gerar um evento” quando algo importante acontecer com ele. O argumento name
é o nome do evento, opcionalmente seguido por argumentos adicionais com dados do evento.
Também o método .on(name, handler)
que adiciona a função handler
como ouvinte de eventos com o nome fornecido. Ele será chamado quando um evento com o name
fornecido for acionado e obterá os argumentos da chamada .trigger
.
…E o método .off(name, handler)
que remove o listener handler
.
Depois de adicionar o mixin, um user
de objeto poderá gerar um evento "login"
quando o visitante fizer login. E outro objeto, digamos, calendar
pode querer escutar tais eventos para carregar o calendário para a pessoa logada.
Ou um menu
pode gerar o evento "select"
quando um item de menu é selecionado, e outros objetos podem atribuir manipuladores para reagir a esse evento. E assim por diante.
Aqui está o código:
deixe eventMixin = { /** * Inscreva-se no evento, uso: * menu.on('selecionar', function(item) { ... } */ on(eventName, manipulador) { if (!this._eventHandlers) this._eventHandlers = {}; if (!this._eventHandlers[nomedoevento]) { this._eventHandlers[nomedoevento] = []; } this._eventHandlers[nomedoevento].push(manipulador); }, /** * Cancele a assinatura, uso: * menu.off('select', manipulador) */ off(eventName, manipulador) { deixe manipuladores = this._eventHandlers?.[eventName]; se (! manipuladores) retornar; for (seja i = 0; i < handlers.length; i++) { if (manipuladores[i] === manipulador) { manipuladores.splice(i--, 1); } } }, /** * Gere um evento com o nome e dados fornecidos * this.trigger('selecionar', dados1, dados2); */ trigger(nomedoevento, ...args) { if (!this._eventHandlers?.[nomedoevento]) { retornar; //não há manipuladores para esse nome de evento } //chama os manipuladores this._eventHandlers[eventName].forEach(handler => handler.apply(this, args)); } };
.on(eventName, handler)
– atribui handler
de função para ser executado quando ocorrer o evento com esse nome. Tecnicamente, existe uma propriedade _eventHandlers
que armazena uma matriz de manipuladores para cada nome de evento e apenas o adiciona à lista.
.off(eventName, handler)
– remove a função da lista de manipuladores.
.trigger(eventName, ...args)
– gera o evento: todos os manipuladores de _eventHandlers[eventName]
são chamados, com uma lista de argumentos ...args
.
Uso:
// Faz uma aula menu de classe { escolher(valor) { this.trigger("selecionar", valor); } } //Adiciona o mixin com métodos relacionados a eventos Object.assign(Menu.prototype, eventMixin); deixe menu = novo Menu(); // adiciona um manipulador, a ser chamado na seleção: menu.on("select", value => alert(`Valor selecionado: ${value}`)); // aciona o evento => o manipulador acima é executado e mostra: // Valor selecionado: 123 menu.choose("123");
Agora, se quisermos que algum código reaja a uma seleção de menu, podemos ouvi-lo com menu.on(...)
.
E eventMixin
mixin facilita adicionar esse comportamento a quantas classes desejarmos, sem interferir na cadeia de herança.
Mixin – é um termo genérico de programação orientada a objetos: uma classe que contém métodos para outras classes.
Algumas outras linguagens permitem herança múltipla. JavaScript não suporta herança múltipla, mas mixins podem ser implementados copiando métodos no protótipo.
Podemos usar mixins como uma forma de aumentar uma classe adicionando vários comportamentos, como manipulação de eventos, como vimos acima.
Os mixins podem se tornar um ponto de conflito se substituirem acidentalmente os métodos de classe existentes. Então geralmente deve-se pensar bem sobre os métodos de nomenclatura de um mixin, para minimizar a probabilidade de isso acontecer.