Extends, super y polimorfismo en JavaScript

En esta página
Enterate del próximo post
Un mail cuando publico algo nuevo. Sin spam y te podés dar de baja cuando quieras.

Un mail cuando publico algo nuevo. Sin spam y te podés dar de baja cuando quieras.
El post anterior cerró dejando pendiente esto mismo: la herencia real, la que reutiliza comportamiento entre varios niveles de la cadena y hace que un mismo método responda distinto según la clase que lo recibe. Ya está explicado qué hace class por dentro (constructor, métodos en el prototipo, new, instanceof). Falta la pieza que conecta una clase con otra.
class Animal {
constructor(nombre) {
this.nombre = nombre;
}
presentarse() {
return `Soy ${this.nombre}`;
}
}
class Perro extends Animal {}
const rex = new Perro("Rex");
rex.presentarse(); // "Soy Rex"
rex instanceof Animal; // true
rex instanceof Perro; // true
Object.getPrototypeOf(Perro.prototype) === Animal.prototype; // truePerro no declara ningún método propio y aun así rex.presentarse() funciona. extends hace, en el momento de definir la clase, exactamente lo que el post de herencia y prototipos mostraba a mano con Object.create: enlaza Perro.prototype con Animal.prototype como su prototipo. La búsqueda de presentarse no encuentra nada en Perro.prototype, sube un escalón, lo encuentra en Animal.prototype. Es la misma cadena de siempre, armada con una palabra clave en vez de una llamada explícita.
Una subclase que necesita su propio dato extra no puede simplemente ignorar el constructor de la base.
class Perro extends Animal {
constructor(nombre, raza) {
super(nombre);
this.raza = raza;
}
presentarse() {
return `${super.presentarse()}, de raza ${this.raza}`;
}
}
const rex = new Perro("Rex", "Labrador");
rex.presentarse(); // "Soy Rex, de raza Labrador"super(nombre) ejecuta el constructor de Animal con this ya enlazado a la instancia de Perro que se está creando, el mismo this que el segundo paso de new arma (visto en el post de class, new e instanceof). Recién después de esa línea existe this para asignarle raza. super.presentarse(), dentro del método, busca presentarse un escalón más arriba de Perro.prototype, en Animal.prototype, en vez de repetir esa línea a mano.
Ese orden no es una sugerencia de estilo: es una regla que el motor exige.
class Gato extends Animal {
constructor(nombre) {
this.nombre = nombre; // sin llamar a super primero
}
}
new Gato("Michi");
// ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructorDentro del constructor de una clase derivada, this no existe todavía. No es undefined como en una función constructora clásica llamada sin new (ver el post de class, new e instanceof): directamente no está disponible hasta que super() termina de ejecutarse. Tocarlo antes es un ReferenceError real, no un valor raro que aparece más tarde.
La cadena de super no se detiene en un solo nivel.
class PerroGuia extends Perro {
constructor(nombre, raza, usuario) {
super(nombre, raza);
this.usuario = usuario;
}
presentarse() {
return `${super.presentarse()}, guía de ${this.usuario}`;
}
}
const rex = new PerroGuia("Rex", "Labrador", "Ana");
rex.presentarse(); // "Soy Rex, guía de Ana"
rex instanceof PerroGuia; // true
rex instanceof Perro; // true
rex instanceof Animal; // truePerroGuia llama a super(nombre, raza), que es el constructor de Perro, que a su vez llama a super(nombre), el de Animal. Cada nivel agrega lo suyo (raza, después usuario) sin repetir lo que el nivel anterior ya resuelve. Y super.presentarse() en PerroGuia tampoco encuentra nada en Perro.prototype (Perro nunca definió su propio presentarse): sigue subiendo un escalón más y lo encuentra en Animal.prototype. La búsqueda no se detiene en el padre inmediato, sigue hasta encontrar la propiedad, sin importar cuántos niveles haga falta cruzar.
class Animal {
constructor(nombre) {
this.nombre = nombre;
}
hacerSonido() {
return "...";
}
}
class Perro extends Animal {
hacerSonido() {
return "Guau";
}
}
class Gato extends Animal {
hacerSonido() {
return "Miau";
}
}
const animales = [new Perro("Rex"), new Gato("Michi")];
animales.map((animal) => `${animal.nombre} dice ${animal.hacerSonido()}`);
// ["Rex dice Guau", "Michi dice Miau"]Perro y Gato pisan hacerSonido cada uno con su propia versión, en su propio prototipo, un escalón antes de llegar al "..." de Animal. El código que arma animales y llama a .map no pregunta de qué clase es cada elemento: llama a hacerSonido() sobre cada uno, y el motor resuelve, subiendo por la cadena de esa instancia en particular, cuál versión ejecutar. Eso es polimorfismo en código: el mismo mensaje (hacerSonido()) produce una respuesta distinta según quién lo recibe, decidido en el momento de la llamada, no de antemano.
Pisar un método no borra el de la base, solo lo tapa: ya se vio en la sección anterior, cuando super.presentarse() seguía encontrando la versión de Animal porque Perro nunca puso la propia. Si Gato tampoco hubiera definido hacerSonido, una instancia de Gato habría heredado directamente el "..." de Animal, sin ningún error ni caso especial.
Object.getPrototypeOf(rex) === PerroGuia.prototype; // true
Object.getPrototypeOf(PerroGuia.prototype) === Perro.prototype; // true
Object.getPrototypeOf(Perro.prototype) === Animal.prototype; // true
Object.getPrototypeOf(Animal.prototype) === Object.prototype; // trueCuatro escalones reales, uno por cada extends más el que ya traía todo objeto desde el principio: la instancia, PerroGuia.prototype, Perro.prototype, Animal.prototype, y por último Object.prototype, donde termina cualquier cadena (visto en el post de herencia y prototipos). Cada extends agrega exactamente un escalón, nada más.
Animal.prototype.despedirse = function () {
return `${this.nombre} se despide`;
};
rex.despedirse(); // "Rex se despide"rex ya existía, dos escalones por debajo de Animal, cuando se agregó despedirse. No hizo falta recrearlo ni tocar Perro ni PerroGuia: la búsqueda por la cadena pasa por Animal.prototype en el momento en que se llama al método, no en el momento en que rex se creó, el mismo comportamiento que ya se vio con objetos comunes delegando en un prototipo compartido. Esto es reuso real: un método escrito una sola vez, en el escalón más alto que lo necesita, disponible para todo lo que ya delega ahí y para todo lo que delegue más adelante, sin copiar una sola línea.
extendsagrega un escalón a la cadena de prototipos que ya existía sinclass;superavanza esa cadena antes de tocarthis; y llamar al mismo método sobre objetos de clases distintas, dejando que cada prototipo resuelva su propia versión, es lo que polimorfismo significa en código, no en la definición de manual.
El próximo post de la serie deja de lado la cadena de prototipos por un momento, para preguntar qué hace compatibles a dos objetos que no comparten ninguna herencia entre sí.