En JavaScript sólo podemos heredar de un único objeto. Sólo puede haber un [[Prototype]]
para un objeto. Y una clase sólo puede extenderse a otra clase.
Pero a veces eso parece limitante. Por ejemplo, tenemos una clase StreetSweeper
y una clase Bicycle
y queremos hacer su combinación: una StreetSweepingBicycle
.
O tenemos una clase User
y una clase EventEmitter
que implementa la generación de eventos, y nos gustaría agregar la funcionalidad de EventEmitter
a User
, para que nuestros usuarios puedan emitir eventos.
Hay un concepto que puede ayudar aquí, llamado "mixins".
Como se define en Wikipedia, un mixin es una clase que contiene métodos que pueden ser utilizados por otras clases sin necesidad de heredar de ella.
En otras palabras, un mixin proporciona métodos que implementan un determinado comportamiento, pero no lo usamos solo, lo usamos para agregar el comportamiento a otras clases.
La forma más sencilla de implementar un mixin en JavaScript es crear un objeto con métodos útiles, de modo que podamos fusionarlos fácilmente en un prototipo de cualquier clase.
Por ejemplo, aquí el mixin sayHiMixin
se usa para agregar algo de "discurso" para User
:
// mezclando digamos HiMixin = { decir Hola() { alert(`Hola ${this.name}`); }, decir adios() { alert(`Adiós ${this.name}`); } }; // uso: usuario de clase { constructor(nombre) { this.nombre = nombre; } } //copiar los métodos Object.assign(Usuario.prototipo, sayHiMixin); // ahora el usuario puede saludar nuevo usuario("Amigo").sayHola(); // ¡Hola amigo!
No hay herencia, sino un método simple de copia. Por lo tanto, User
puede heredar de otra clase y también incluir el mixin para “mezclar” los métodos adicionales, como este:
clase Usuario extiende Persona { //... } Object.assign(Usuario.prototipo, sayHiMixin);
Los mixins pueden hacer uso de la herencia dentro de ellos mismos.
Por ejemplo, aquí sayHiMixin
hereda de sayMixin
:
digamosMixin = { decir (frase) { alerta(frase); } }; digamos HiMixin = { __proto__: sayMixin, // (o podríamos usar Object.setPrototypeOf para configurar el prototipo aquí) decir Hola() { //llamar al método padre super.say(`Hola ${this.name}`); // (*) }, decir adios() { super.say(`Adiós ${this.name}`); // (*) } }; usuario de clase { constructor(nombre) { this.nombre = nombre; } } //copiar los métodos Object.assign(Usuario.prototipo, sayHiMixin); // ahora el usuario puede saludar nuevo usuario("Amigo").sayHola(); // ¡Hola amigo!
Tenga en cuenta que la llamada al método principal super.say()
desde sayHiMixin
(en las líneas etiquetadas con (*)
) busca el método en el prototipo de ese mixin, no la clase.
Aquí está el diagrama (ver la parte derecha):
Esto se debe a que los métodos sayHi
y sayBye
se crearon inicialmente en sayHiMixin
. Entonces, aunque fueron copiados, su propiedad interna [[HomeObject]]
hace referencia sayHiMixin
, como se muestra en la imagen de arriba.
Como super
busca métodos principales en [[HomeObject]].[[Prototype]]
, eso significa que busca sayHiMixin.[[Prototype]]
.
Ahora hagamos una mezcla para la vida real.
Una característica importante de muchos objetos del navegador (por ejemplo) es que pueden generar eventos. Los eventos son una excelente manera de "transmitir información" a cualquiera que la desee. Entonces, hagamos un mixin que nos permita agregar fácilmente funciones relacionadas con eventos a cualquier clase/objeto.
El mixin proporcionará un método .trigger(name, [...data])
para "generar un evento" cuando le suceda algo importante. El argumento name
es el nombre del evento, seguido opcionalmente de argumentos adicionales con datos del evento.
También el método .on(name, handler)
que agrega la función handler
como escucha de eventos con el nombre de pila. Se llamará cuando se active un evento con el name
de pila y obtendrá los argumentos de la llamada .trigger
.
…Y el método .off(name, handler)
que elimina el oyente handler
.
Después de agregar el mixin, un user
de objeto podrá generar un evento "login"
cuando el visitante inicie sesión. Y otro objeto, por ejemplo, calendar
puede querer escuchar dichos eventos para cargar el calendario de la persona que inició sesión.
O bien, un menu
puede generar el evento "select"
cuando se selecciona un elemento del menú, y otros objetos pueden asignar controladores para reaccionar ante ese evento. Etcétera.
Aquí está el código:
dejar eventoMixin = { /** * Suscríbete al evento, uso: * menu.on('seleccionar', función(elemento) {...} */ en (nombre del evento, controlador) { if (!this._eventHandlers) this._eventHandlers = {}; if (!this._eventHandlers[nombre del evento]) { this._eventHandlers[nombre del evento] = []; } this._eventHandlers[nombre del evento].push(handler); }, /** * Cancelar la suscripción, uso: * menu.off('seleccionar', controlador) */ off(nombre del evento, controlador) { let handlers = this._eventHandlers?.[nombre del evento]; si (!handlers) regresan; for (let i = 0; i < handlers.length; i++) { if (controladores[i] === controlador) { handlers.splice(i--, 1); } } }, /** * Generar un evento con el nombre y datos dados * this.trigger('seleccionar', datos1, datos2); */ disparador (nombre del evento, ... argumentos) { if (!this._eventHandlers?.[nombre del evento]) { devolver; // no hay controladores para ese nombre de evento } //llamar a los manejadores this._eventHandlers[nombre del evento].forEach(handler => handler.apply(this, args)); } };
.on(eventName, handler)
: asigna handler
de función para que se ejecute cuando ocurre el evento con ese nombre. Técnicamente, existe una propiedad _eventHandlers
que almacena una serie de controladores para cada nombre de evento y simplemente lo agrega a la lista.
.off(eventName, handler)
: elimina la función de la lista de controladores.
.trigger(eventName, ...args)
– genera el evento: se llaman todos los controladores de _eventHandlers[eventName]
, con una lista de argumentos ...args
.
Uso:
//Hacer una clase Menú de clase { elegir (valor) { this.trigger("seleccionar", valor); } } // Agrega el mixin con métodos relacionados con eventos Object.assign(Menú.prototype, eventMixin); dejar menú = nuevo Menú(); // agrega un controlador, que se llamará en la selección: menu.on("select", valor => alerta(`Valor seleccionado: ${valor}`)); // desencadena el evento => el controlador anterior se ejecuta y muestra: // Valor seleccionado: 123 menú.elegir("123");
Ahora, si queremos que algún código reaccione a una selección de menú, podemos escucharlo con menu.on(...)
.
Y eventMixin
mixin facilita agregar dicho comportamiento a tantas clases como queramos, sin interferir con la cadena de herencia.
Mixin : es un término genérico de programación orientada a objetos: una clase que contiene métodos para otras clases.
Algunos otros idiomas permiten la herencia múltiple. JavaScript no admite herencia múltiple, pero los mixins se pueden implementar copiando métodos en un prototipo.
Podemos usar mixins como una forma de aumentar una clase agregando múltiples comportamientos, como el manejo de eventos, como hemos visto anteriormente.
Los mixins pueden convertirse en un punto de conflicto si accidentalmente sobrescriben métodos de clase existentes. Por lo general, uno debería pensar bien en los métodos de denominación de un mixin, para minimizar la probabilidad de que eso suceda.