Herencia y composición en JavaScript: cuándo usar cada una | Nicolás Garzón
La herencia permite que una clase derive comportamiento de otra. La composición construye un objeto combinando colaboradores o capacidades.
JavaScript
Copiar class Employee {
work ( ) {
return "Working" ;
}
}
class Developer extends Employee {
writeCode ( ) {
return "Writing code" ;
}
} JavaScript
Copiar function createDeveloper ( { work, writeCode } ) {
return {
work,
writeCode,
} ;
} JavaScript
Copiar class Manager extends Employee { } extends conecta dos relaciones prototípicas.
Texto
Copiar manager
→ Manager.prototype
→ Employee.prototype
→ Object.prototype
→ nullTexto
Copiar Manager
→ Employee
→ Function.prototype
→ Object.prototype
→ nullPor eso la clase hija puede heredar métodos de instancia y miembros estáticos.
JavaScript
Copiar class Employee {
constructor ( name ) {
this . name = name;
}
}
class Manager extends Employee {
constructor ( name, department ) {
super ( name) ;
this . department = department;
}
} En una clase derivada, no puedes usar this antes de llamar super().
JavaScript
Copiar constructor ( name ) {
this . name = name;
super ( name) ;
} super() ejecuta el constructor padre y prepara la instancia derivada.
Si la clase hija no declara constructor, utiliza uno equivalente a:
JavaScript
Copiar constructor ( ... args) {
super ( ... args) ;
} JavaScript
Copiar class Employee {
describe ( ) {
return ` Employee: ${ this . name} ` ;
}
}
class Manager extends Employee {
describe ( ) {
return ` Manager: ${ this . name} ` ;
}
} La propiedad de Manager.prototype aparece primero en la cadena y oculta la de Employee.prototype.
JavaScript
Copiar class Manager extends Employee {
describe ( ) {
return ` ${ super . describe ( ) } | Department: ${ this . department} ` ;
}
} super.describe() busca el método en el prototipo padre, pero conserva la instancia actual como receptor.
Dentro de la llamada, this sigue siendo el objeto Manager.
super depende del método y del objeto donde fue definido. No se puede guardar o reasignar como una variable normal.
Los métodos creados con sintaxis de método conservan información interna sobre su hogar para resolver super.
JavaScript
Copiar const managerMethods = {
describe ( ) {
return super . describe ( ) ;
} ,
} ; Su comportamiento depende del prototipo del objeto que contiene el método, no únicamente del receptor de la llamada.
No conviertas super en una herramienta de metaprogramación sin comprender esa relación.
Los campos de instancia son propiedades propias, no métodos del prototipo.
JavaScript
Copiar class Base {
label = "base" ;
}
class Child extends Base {
label = "child" ;
} Durante la construcción derivada, el campo de la hija reemplaza el valor creado por la base.
super.label no accede al campo de instancia de la base, porque ese campo vive en this, no en Base.prototype.
JavaScript
Copiar class BaseReport {
constructor ( ) {
this . configure ( ) ;
}
configure ( ) { }
}
class SalesReport extends BaseReport {
data = [ ] ;
configure ( ) {
this . data. push ( "sales" ) ;
}
} El constructor base puede llamar la versión sobrescrita antes de que los campos de la hija hayan sido inicializados, lo que produce errores difíciles de detectar.
Evita llamar métodos sobrescribibles desde constructores. Mantén la construcción predecible.
JavaScript
Copiar class Entity {
static type = "entity" ;
static readType ( ) {
return this . type;
}
}
class Product extends Entity {
static type = "product" ;
} JavaScript
Copiar Product. readType ( ) ; La clase hija encuentra readType mediante la cadena de la clase y lo ejecuta con Product como receptor.
JavaScript
Copiar class Account {
#balance = 0 ;
readBalance ( ) {
return this . #balance;
}
}
class SavingsAccount extends Account { } La instancia hija tiene la parte privada creada por la base y puede usar el método heredado.
Pero SavingsAccount no puede mencionar directamente #balance. La clase base debe exponer una operación apropiada.
Distintos objetos pueden responder al mismo mensaje.
JavaScript
Copiar class CardPayment {
pay ( amount ) {
return ` Paid ${ amount} by card ` ;
}
}
class CashPayment {
pay ( amount ) {
return ` Paid ${ amount} in cash ` ;
}
}
function completePayment ( paymentMethod, amount ) {
return paymentMethod. pay ( amount) ;
} completePayment depende del comportamiento pay, no del tipo concreto.
JavaScript también permite polimorfismo sin herencia.
JavaScript
Copiar const transferPayment = {
pay ( amount ) {
return ` Transferred ${ amount} ` ;
} ,
} ; Esto suele llamarse duck typing: importa el contrato observable.
Si una función espera un PaymentMethod, cualquier implementación debería respetar el mismo contrato.
Una subclase problemática:
JavaScript
Copiar class BrokenPayment extends CardPayment {
pay ( ) {
throw new Error ( "Payment is disabled" ) ;
}
} Aunque hereda técnicamente, ya no puede sustituir al padre en contextos que esperan un pago válido.
La herencia debe preservar expectativas sobre:
Argumentos.
Retornos.
Errores.
Estado.
Efectos.
No basta con reutilizar código.
Herencia suele modelar una relación estable de tipo:
Texto
Copiar Manager es un EmployeeComposición modela colaboración:
Texto
Copiar OrderService tiene un PaymentGatewayJavaScript
Copiar class OrderService {
constructor ( paymentGateway ) {
this . paymentGateway = paymentGateway;
}
complete ( order ) {
return this . paymentGateway. pay ( order. total) ;
}
} OrderService no necesita heredar de PaymentGateway; lo utiliza.
JavaScript
Copiar function createOrderService ( {
paymentGateway,
inventory,
notifier,
} ) {
return {
async complete ( order ) {
await inventory. reserve ( order. items) ;
const payment = await paymentGateway. pay (
order. total,
) ;
await notifier. sendConfirmation ( order) ;
return payment;
} ,
} ;
} Las capacidades se entregan explícitamente.
Sustitución sencilla en pruebas.
Menos acoplamiento a una jerarquía.
Dependencias visibles.
Combinaciones diferentes por entorno.
JavaScript
Copiar const canActivate = ( state ) => ( {
activate ( ) {
state. isActive = true ;
} ,
} ) ;
const canRename = ( state ) => ( {
rename ( name ) {
state. name = name;
} ,
} ) ;
function createProduct ( name ) {
const state = {
name,
isActive : false ,
} ;
return {
read ( ) {
return { ... state } ;
} ,
... canActivate ( state) ,
... canRename ( state) ,
} ;
} Esta composición usa closures para compartir estado.
Debes definir qué ocurre si dos capacidades tienen propiedades con el mismo nombre. El último spread sobrescribe al anterior.
La composición no siempre requiere spread.
JavaScript
Copiar class ReportService {
constructor ( formatter ) {
this . formatter = formatter;
}
create ( data ) {
return this . formatter. format ( data) ;
}
} El servicio delega el trabajo al formatter sin copiar sus métodos.
Esta forma mantiene clara la identidad y evita colisiones.
Un mixin copia o añade comportamiento a una clase.
JavaScript
Copiar const Timestamped = ( Base ) =>
class extends Base {
createdAt = new Date ( ) ;
} ;
class Entity { }
class Product extends Timestamped ( Entity) { } Los mixins pueden ser útiles, pero varias capas pueden producir:
Jerarquías difíciles de inspeccionar.
Colisiones de nombres.
Orden de aplicación importante.
Tipos y stack traces complejos.
No los uses solo para evitar escribir una función colaboradora.
Texto
Copiar Entity
→ User
→ StaffUser
→ Manager
→ RegionalManagerUna jerarquía profunda puede crear dependencias implícitas entre muchos niveles.
Cambiar la base afecta a todas las hijas.
Es difícil saber dónde se define un método.
Las subclases sobrescriben comportamiento inesperadamente.
Una entidad puede necesitar capacidades de ramas diferentes.
Prefiere jerarquías pequeñas y estables. Para variaciones combinables, considera composición.
JavaScript
Copiar class ProductList extends Array {
active ( ) {
return this . filter (
( product ) => product. isActive,
) ;
}
} Es posible extender clases nativas, pero debes conocer:
Qué métodos crean nuevas instancias.
El comportamiento de Symbol.species.
Serialización.
Expectativas de APIs externas.
Costos de una abstracción no estándar.
A menudo una clase que contiene un array es más explícita.
JavaScript
Copiar class ProductCollection {
constructor ( products = [ ] ) {
this . products = products;
}
}
Existe una relación “es un” estable.
La clase hija respeta el contrato de la base.
La jerarquía es corta.
El polimorfismo de tipo aporta valor.
La base fue diseñada para extensión.
Una entidad “tiene” o “usa” colaboradores.
Las capacidades deben combinarse.
Las dependencias cambian por entorno.
Quieres sustituir comportamiento en pruebas.
La reutilización no representa una relación de tipo.
JavaScript
Copiar class EmailNotifier {
send ( message ) {
return emailClient. send ( message) ;
}
}
class SlackNotifier {
send ( message ) {
return slackClient. send ( message) ;
}
}
class AlertService {
constructor ( notifier ) {
this . notifier = notifier;
}
sendCriticalAlert ( message ) {
return this . notifier. send ( {
severity : "critical" ,
message,
} ) ;
}
} AlertService compone un notifier. No necesita heredar de cada canal.
Usar herencia únicamente para evitar duplicar código.
Crear una clase hija que rompe el contrato del padre.
Usar this antes de super().
Creer que super.field accede a campos propios de la instancia base.
Llamar métodos sobrescribibles desde constructores.
Diseñar jerarquías profundas y frágiles.
Confundir polimorfismo con herencia obligatoria.
Copiar capacidades con spread sin revisar colisiones.
Utilizar mixins en capas difíciles de rastrear.
Presentar composición como regla absoluta para todos los dominios.
extends conecta la cadena de instancias y la cadena de clases.
super() debe ejecutarse antes de usar this en un constructor derivado.
super.method() conserva la instancia actual como receptor.
Sobrescribir debe mantener el contrato esperado.
El polimorfismo puede existir sin herencia.
Herencia modela una relación de tipo; composición modela colaboración.
Las dependencias explícitas facilitan sustitución y pruebas.
Las jerarquías cortas suelen ser más comprensibles.
Clases, prototipos, factories y composición pueden coexistir.
¿Cuál diseño representa mejor la relación?
Texto
Copiar OrderService necesita utilizar un PaymentGateway.Respuesta Composición. OrderService tiene o utiliza un PaymentGateway; no es un tipo de payment gateway. Puede recibirlo como dependencia.
Object.freeze y Object.seal estudia restricciones aplicadas a la estructura de un objeto y sus límites superficiales.