Ir al contenido
wannacode

Herencia

En resumen: La herencia es cuando una clase (la hija) recibe los campos y los métodos de otra clase (la clase padre), así que la parte común se escribe en un solo lugar.

Problema

Dos copias del mismo código

El programa de nóminas tiene una clase Employee con un nombre, un salario y un método para un aumento. Después hizo falta también un gerente, que además recibe un bono. Lo más rápido fue copiar Employee en una clase nueva Manager y añadir el bono.

Un mes después alguien introduce un aumento del -10 % en lugar del 10 %. Así que añades una comprobación a Employee. Te olvidas de ella en Manager:

// en Employee
raise(percent) {
if (percent > 0) {
this.#salary += Math.trunc(this.#salary * percent / 100);
}
}
// en Manager: copia de antes de la comprobación
raise(percent) {
this.#salary += Math.trunc(this.#salary * percent / 100);
}
const ivan = new Employee("Juan", 2000);
const maria = new Manager("María", 3000, 500);
ivan.raise(-10);
maria.raise(-10);
console.log(String(ivan));
console.log(String(maria));
Salida
Juan: 2000 €
María: 2700 €

El salario de Juan está protegido, pero el de María bajó 300 €. Las dos clases son iguales en un 90 %, y aun así cada corrección hay que hacerla dos veces.

La idea

La parte común va en la clase padre

Un gerente es un empleado, solo que con algo más. Dilo en el código: class Manager extends Employee. En Manager escribes solo lo nuevo, el bono:

class Manager extends Employee {
#bonus;
constructor(name, salary, bonus) {
super(name, salary);
this.#bonus = bonus;
}
get bonus() {
return this.#bonus;
}
}
const maria = new Manager("María", 3000, 500);
maria.raise(-10);
maria.raise(10);
console.log(String(maria));
console.log(`Bono: ${maria.bonus} €`);
Salida
María: 3300 €
Bono: 500 €

Ahora Manager tiene todo lo que tiene Employee: un nombre, un salario, raise() con la comprobación, toString(). Por eso el aumento del -10 % no pasó. La comprobación está escrita una vez y funciona para las dos clases.

Esto se llama herencia. Employee es la clase padre (clase base), y Manager es la clase hija. La hija lo toma todo de la clase padre y añade lo suyo.

Para comprobar si la herencia tiene sentido, usa la frase «X es un Y». «Un gerente es un empleado» es cierto. «Una factura es un empleado» no lo es, así que la factura no hereda de Employee, aunque tenga un importe y un nombre.

Pruébalo

¿Qué recibe el becario?

Intern no tiene nada propio: el cuerpo está vacío, {}.

¿Qué imprimirá el programa?

class Intern extends Employee {}
const ana = new Intern("Ana", 1200);
ana.raise(5);
console.log(String(ana));
Respuesta

La idea

Primero la clase padre

#name y #salary están ocultos en Employee. La hija los tiene, pero no los ve: para la hija están ocultos, igual que para cualquier otra clase. Entonces, ¿cómo les da un valor Manager?

Llamando al constructor de la clase padre:

constructor(name, salary, bonus) {
super(name, salary);
this.#bonus = bonus;
}

super(name, salary) llama al constructor de Employee con estos argumentos. Prepara #name y #salary, y después Manager prepara #bonus. En JavaScript, super(...) tiene que llamarse antes de que el constructor toque this. Si lo olvidas, new Manager(...) se detiene con ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor.

Hay un solo objeto. Contiene tanto la parte de la clase padre como la de la hija, y cada parte la prepara su propio constructor.

constructor(name, salary, bonus) {
super(name, salary);
this.#bonus = bonus;
}
const maria = new Manager("María", 3000, 500);
console.log(`${maria}, bono ${maria.bonus} €`);
Paso a paso, en palabras
  1. new crea un objeto. Contendrá la parte de Employee y la parte de Manager.
  2. Empieza el constructor de Manager con los tres argumentos.
  3. Primero la clase padre: super(name, salary) llama al constructor de Employee. Antes de eso no se puede tocar this.
  4. El constructor de Employee prepara su parte: #name y #salary.
  5. Solo entonces el cuerpo de Manager prepara el bono.
  6. El objeto es uno solo, y cada parte la prepara su propio constructor.
Más precisamente, para curiosos
  • El constructor se hereda. Por eso Intern, sin un constructor propio, toma el nombre y el salario de Employee: JavaScript llama por ti a la clase padre con los argumentos que pasaste.
  • En cuanto escribes tu propio constructor, super(...) es cosa tuya. Los campos de la hija (#bonus) se crean solo después de que termine super(...).

Pruébalo

¿Qué constructor se ejecuta primero?

Aquí la hija no llama a la clase padre de forma explícita:

¿Qué imprimirá el programa?

class Account {
constructor() {
console.log("Cuenta abierta");
}
}
class SavingsAccount extends Account {
constructor() {
console.log("Interés: 3%");
}
}
const savings = new SavingsAccount();
Respuesta

Pruébalo

Encuentra el error

María debería tener un salario de 3000 € y un bono de 500 €. Pero el programa imprime:

Salida
María: 500 €

¿En qué línea está el error?

Elige la línea con el error.

La idea

Sin protected: un guion bajo

A fin de año, el bono del gerente se suma al salario. El método está en Manager, pero el salario es #salary en Employee. El getter solo lo lee, y aquí hay que cambiarlo. Y this.#salary no está permitido en Manager: el programa ni siquiera arranca, SyntaxError: Private field '#salary' must be declared in an enclosing class.

En Java y C# hay un tercer nivel de acceso para esto, protected: la clase y sus hijas lo ven, y todos los demás no. JavaScript no lo tiene. En su lugar escribes el campo con un guion bajo, _salary: una convención para «interno, para la clase y sus hijas» que el lenguaje no comprueba:

class Employee {
#name;
constructor(name, salary) {
this.#name = name;
this._salary = salary;
}
// ... el resto, como antes, con this._salary
}
class Manager extends Employee {
// ... #bonus y el constructor
mergeBonus() {
this._salary += this.#bonus;
this.#bonus = 0;
}
}
const maria = new Manager("María", 3000, 500);
maria.mergeBonus();
console.log(String(maria));
console.log(`Bono: ${maria.bonus} €`);
Salida
María: 3500 €
Bono: 0 €

La convención no es una protección: maria._salary = 0 en el programa también pasa, sin ningún error. Así que todo lo que la hija no debe tocar se queda en # (#name), y solo el campo que realmente tiene que cambiar lleva un guion bajo.

Más precisamente, para curiosos
  • # es una protección real: no se puede llegar al campo ni desde fuera ni desde las hijas. Por eso también la comprobación ocurre antes de que arranque el programa, no mientras se ejecuta.

Pruébalo

¿Qué falta?

Un becario es un empleado. ¿Qué va en el hueco?

class Intern  Employee {}

La idea

Una clase padre y un antepasado común

Una clase tiene exactamente una clase padre. No puede heredar a la vez de Employee y de Student. Por eso la herencia es simple.

La cadena puede ser larga. Intern hereda de Employee. ¿Y de qué hereda Employee, si no tiene extends? Toda clase sin padre hereda de Object. Es el padre común de todas las clases. Por eso incluso una clase vacía tiene métodos:

class Ticket {}
const ticket = new Ticket();
console.log(ticket.toString());
console.log(ticket);
Salida
[object Object]
Ticket {}

toString() viene de Object y devuelve [object Object]. En el módulo «Clases y objetos» escribiste tu propio toString(). Ahora sabes que así reemplazas la versión de Object. Hay más sobre esto en la lección siguiente.

console.log(ticket) no llama a toString(). Muestra la clase y los campos: Ticket {}. Por eso en esta lección imprimimos String(maria): String() llama a toString().

Más precisamente, para curiosos
  • En JavaScript no hay ninguna clase de la que no se pueda heredar.
  • class WorkingStudent extends Employee, Student no arranca: SyntaxError. Después de extends va exactamente una clase.

Ejercicio

Clases hijas

Resuelve las tareas en tu editor. Las clases padre son del módulo «Clases y objetos». Si no las tienes a mano, escríbelas otra vez: son cortas.

1. Coche familiar. SimpleCar tiene una velocidad y un método accelerate() que la sube en 10. Escribe FamilyCar, que hereda de SimpleCar, empieza con 4 asientos y tiene un método addSeat(). Crea un coche familiar, acelera y añade un asiento:

Salida
Velocidad: 10 km/h
Asientos: 5

2. Estudiante. Person tiene un nombre, una edad e introduce(). Escribe Student, que hereda de Person y añade una nota media grade (un número decimal). Usa el constructor de la clase padre.

Salida
Soy Elena, tengo 19 años.
Nota media: 5.5

3. Breakdance. DanceMove tiene un nombre, una duración en segundos y perform(). Escribe BreakDanceMove con una complejidad del 1 al 10 y un método showComplexity().

Salida
Ejecuto moonwalk durante 5 segundos
Ejecuto windmill durante 12 segundos
windmill: complejidad 8 de 10

4. Coche eléctrico. Car tiene una marca, combustible en litros y los kilómetros recorridos. refuel(liters) añade combustible, y drive(distance) gasta 1 litro cada 10 km y suma los kilómetros (si no hay combustible suficiente, lo dice y no se mueve). Escribe ElectricCar:

  • sin combustible, y con una batería que empieza al 100 %;
  • con una eficiencia: cuántos km recorre con el 1 % de batería (5 para el «Tesla»);
  • charge() carga la batería al 100 %;
  • driveElectric(distance) gasta batería en lugar de combustible y también suma los kilómetros.
Si los kilómetros son #mileage en Car, la hija no puede cambiarlos. Pista: la tarjeta sobre el guion bajo.
Salida
Škoda: 150 km, combustible 35 l
Tesla: 200 km, batería 60%
No alcanza la batería para 400 km
Tesla: 600 km, batería 20%

Más tareas

  • Trabajador. Worker hereda de Person y añade un puesto de trabajo y un método work():
Salida
Soy Pablo, tengo 34 años.
Trabajo como contable
  • Becario con mentor. Añade a Intern un campo con el nombre del mentor y pásalo en el constructor, junto con el nombre y el salario para la clase padre. Ahora Intern sí necesita su propio constructor.

Test

Ponte a prueba

1. class Manager extends Employee. ¿Qué es cierto?
2. El campo #name está en Employee. ¿Puede Manager escribir this.#name = "María" en uno de sus métodos?
3. ¿Qué hace super(name, salary) en el constructor de Manager?
4. ¿Cuántas clases padre puede tener una clase?
5. JavaScript no tiene protected. ¿Cómo escribes un campo que usan las hijas y que todos los demás deben dejar en paz?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·