Pregunta 1¿Qué devuelve instanceof Document para minutes, en el que se copiaron piezas con Object.assign?
Composición y herencia — Object.assign y mixins
Divide las funcionalidades en piezas y combínalas en un solo objeto. Verás Object.assign, funciones que devuelven piezas, funciones mixin y cuándo conviene la herencia.
El sistema de documentos de tu empresa necesita exportar a PDF las actas de reunión, aprobar los contratos y hacer ambas cosas con los presupuestos. Después de extends solo puedes poner un padre, así que terminas escribiendo una clase para cada combinación de funcionalidades.
En este artículo verás la composición, que divide las funcionalidades en piezas y las agrega a los objetos, y las funciones mixin, que apilan funcionalidades sobre una clase.
Agregar solo las piezas que necesita cada documento — Object.assign
Supón que creaste una clase ApprovableDocument que permite aprobar y, como los presupuestos también necesitan exportarse a PDF, otra clase ExportableDocument que hereda de ella. Las actas solo necesitan el PDF, así que o heredan una aprobación que nunca usan, o extienden Document por separado y copian el código del PDF.
Con la composición (armar un objeto a partir de grupos de métodos separados, un grupo por funcionalidad), le pasas las piezas a Object.assign(destino, pieza1, pieza2). Copia en el primer argumento las propiedades de todos los demás y devuelve ese mismo primer objeto.
class Document {
constructor(title) { this.title = title; }
}
// Crea un grupo de métodos (una pieza) por funcionalidad
const approvable = {
approve(name) { return `${this.title}: aprobado por ${name}`; },
};
const exportable = {
toPdfName() { return `${this.title}.pdf`; },
};
// Copia en cada documento solo las piezas que necesita
const minutes = Object.assign(new Document("Acta de la reunión semanal"), exportable);
const estimate = Object.assign(new Document("Presupuesto de mantenimiento"), approvable, exportable);
console.log(Object.keys(estimate).join(", ")); // title, approve, toPdfName
console.log(estimate.approve("Ana")); // Presupuesto de mantenimiento: aprobado por Ana
console.log(minutes.toPdfName()); // Acta de la reunión semanal.pdf
console.log(typeof minutes.approve); // undefined (el acta no tiene aprobación)
En estimate.approve("Ana"), estimate está a la izquierda del punto, así que this dentro de approve es estimate y this.title es el título del presupuesto. El destino sigue siendo una instancia de new Document, por lo que instanceof Document también da true.
Comentarios propios para cada documento — funciones que devuelven piezas
Ahora quieres agregar también una lista de comentarios, así que conviertes { comments: [], addComment(text) { ... } } en una pieza y la copias en el acta y en el presupuesto con Object.assign. Lo que se copia es solo la referencia al array, así que el comments de ambos documentos apunta al mismo array, y un comentario agregado al acta también aparece en la lista del presupuesto.
Si cambias la pieza de comentarios por commentable, una función que crea y devuelve una pieza nueva cada vez que la llamas, el [] de comments es un array distinto en cada llamada. Convierte también approvable en función y escribe { ...approvable(), ...commentable() }: la sintaxis spread copia las claves y los valores de cada pieza en un objeto nuevo.
// Una función que devuelve una pieza: cada llamada crea un objeto y un array nuevos
const commentable = () => ({
comments: [],
addComment(text) { this.comments.push(text); },
});
const approvable = () => ({
approvedBy: "pendiente",
approve(name) { this.approvedBy = name; },
});
// Combina las piezas con spread para crear un documento nuevo
const minutes = { title: "Acta de la reunión semanal", ...commentable() };
const estimate = { title: "Presupuesto de mantenimiento", ...approvable(), ...commentable() };
minutes.addComment("Próxima: el 10");
estimate.approve("Ana");
console.log(minutes.comments.length, estimate.comments.length); // 1 0 (arrays distintos)
console.log(estimate.approvedBy); // Ana
console.log(Object.keys(estimate).join(", ")); // title, approvedBy, approve, comments, addComment
El objeto nuevo lo crea el literal { }; la sintaxis spread solo copia en él las claves del valor que devuelve cada pieza. A diferencia de Object.assign, no preparas un destino antes, así que lo que obtienes es un objeto plano, no una instancia de una clase.
Si una clave se repite, gana la última pieza
Tanto con Object.assign como con la sintaxis spread, cuando dos piezas comparten el nombre de una clave, se queda el valor de la pieza que pasaste después. Si approvable() y commentable() tuvieran los dos un describe, solo quedaría el último, sin ningún error, así que evita que los nombres de métodos se repitan entre piezas.
Apilar funcionalidades en la definición de una clase — funciones mixin
En otra parte del sistema, los presupuestos se definen como class Estimate y se crean con new Estimate(...) en cada pantalla. Si agregas la aprobación y los comentarios después con Object.assign, tienes que copiarlos después de cada new, y si te olvidas en algún presupuesto, llamar a approve lanza un TypeError.
Una función mixin (una función que recibe una clase y devuelve otra que la extiende con funcionalidades extra) se escribe como (Base) => class extends Base { ... }. class extends Base { ... } es una expresión de clase sin nombre. Después de extends puedes escribir una llamada a esta función, y la clase que devuelve pasa a ser el padre.
class Document {
constructor(title) { this.title = title; }
describe() { return this.title; }
}
// Recibe una clase y devuelve una clase que la extiende con funcionalidades extra
const Approvable = (Base) => class extends Base {
approve(name) { this.approvedBy = name; }
describe() { return `${super.describe()} (${this.approvedBy ?? "pendiente"})`; }
};
const Commentable = (Base) => class extends Base {
comments = []; // Un array nuevo para cada instancia
addComment(text) { this.comments.push(text); }
};
// Las dos se apilan en la definición de la clase, así que no hay nada que copiar tras cada new
class Estimate extends Commentable(Approvable(Document)) {}
const estimate = new Estimate("Presupuesto de mantenimiento");
estimate.approve("Ana");
console.log(estimate.describe()); // Presupuesto de mantenimiento (Ana)
En Approvable, super.describe() llama al describe de Document, que es el Base que recibió. Aunque inviertas el orden y escribas Approvable(Commentable(Document)), igual obtienes la aprobación y los comentarios.
Extender un mixin sin llamarlo lanza un TypeError
extends Approvable convierte en padre a la propia función flecha. Una función flecha no puede ser el padre de una clase, así que se lanza TypeError: Class extends value ... is not a constructor or null. Escribe Approvable(Document) como padre.
Comparar los dos enfoques — mixins frente a extends
A medida que escribes una función mixin por cada funcionalidad, los paréntesis empiezan a anidarse, como en class Estimate extends Commentable(Approvable(Document)) {}. Cuantas más apilas, más difícil es saber, con solo leer esa línea de declaración, de qué tipo es Estimate y qué funcionalidades se le agregaron encima.
Compáralo con una versión con herencia, en la que CommentableDocument (comentarios) extiende ApprovableDocument (aprobación). Con cualquiera de los dos enfoques, instanceof Document da true y puedes usar super. La diferencia está en si puedes elegir las funcionalidades de cada clase.
class Document { constructor(title) { this.title = title; } }
const Commentable = (Base) => class extends Base { // El mismo mixin de la sección anterior
comments = []; addComment(text) { this.comments.push(text); }
};
// Herencia: la clase de comentarios se construye sobre la de aprobación
class ApprovableDocument extends Document {
approve(name) { this.approvedBy = name; }
}
class CommentableDocument extends ApprovableDocument {
comments = [];
addComment(text) { this.comments.push(text); }
}
const oldMinutes = new CommentableDocument("Acta de la reunión semanal");
console.log(typeof oldMinutes.approve); // function (hereda una aprobación que no usa)
// Apilar con un mixin
class Minutes extends Commentable(Document) {}
const minutes = new Minutes("Acta de la reunión semanal");
console.log(typeof minutes.approve); // undefined (solo se agregan los comentarios)
console.log(minutes instanceof Document, oldMinutes instanceof Document); // true true
Las funcionalidades que algunos tipos de objeto necesitan y otros no, como la aprobación y los comentarios, ponlas en piezas, y usa extends para las relaciones que forman una sola línea de padre a hijo, como «un presupuesto es un tipo de documento». La tabla de abajo reúne los cuatro enfoques de este artículo.
| Enfoque | Cómo se agregan | Útil para |
|---|---|---|
| extends Padre | Hereda todo del padre | Relación «es un tipo de» |
| Object.assign(destino, pieza) | Copia en un objeto existente | Ampliar un objeto que ya tienes |
| { ...pieza() } | Copia en un objeto nuevo | Armar sin clases |
| extends Mixin(Padre) | Apila lo que eliges | Agregar en la definición |
Verificación de conocimientos
Responde cada pregunta una a una.
Pregunta 2Si copias el comments: [] de una misma pieza en dos documentos con Object.assign y haces push en uno, ¿qué pasa?
Pregunta 3En Commentable(Approvable(Document)), ¿qué clase extiende directamente a Document?