L’ereditarietà in javascript è una caratteristica che permette ad una classe di estendere le proprietà di altre classi.
LA PAROLA CHIAVE EXTENDS
Ipotizziamo di avere una classe Animal:
class Animal {
constructor(name) {
this.speed = 0;
this.name = name;
}
run(speed) {
this.speed = speed;
alert(`${this.name} runs with speed ${this.speed}.`);
}
stop() {
this.speed = 0;
alert(`${this.name} stands still.`);
}
}
let animal = new Animal(“My animal“);
Qui vediamo come rappresentare l’oggetto Animal e la classe Animal graficamente:
Potremmo voler creare un’altra class Rabbit. Poiché i conigli sono animali, la classe Rabbit dovrebbe essere basata su Animal, avendo accesso a tutti i metodi di Animal, in questo modo Rabbit può assumere tutti i comportamenti di base di un Animal. Creiamo una class Rabbit che eredita da Animal:
class Rabbit extends Animal {
hide() {
alert(`${this.name} hides!`);
}
}
let rabbit = new Rabbit(“White Rabbit“);
rabbit.run(5); // White Rabbit runs with speed 5.
rabbit.hide(); // White Rabbit hides!
L’oggetto della classe Rabbit ha accesso sia ai metodi di Rabbit (ad esempio rabbit.hide()) che a quelli di Animal (rabbit.run()). Internamente, extends aggiunge da Rabbit.prototype un riferimento [[Prototype]] ad Animal.prototype:
Ad esempio, per trovare il metodo rabbit.run, il motore JavaScript controlla (dal basso verso l’alto in figura):
- L’oggetto rabbit (non possiede run).
- Il suo prototype, che è Rabbit.prototype (possiede hide, ma non run).
- Il suo prototype, che è (a causa di extends) Animal.prototype, che possiede il metodo run.
Come ricordiamo dai Native prototypes, JavaScript stesso usa l’ereditarietà per prototipi per gli oggetti integrati. Ad esempio Date.prototype.[[Prototype]] è Object.prototype. Questo è il motivo per cui le date hanno accesso ai metodi generici di un oggetto.
SOVRASCRIVERE UN METODO
Proseguiamo ora e vediamo come sovrascrivere un metodo. Di base, tutti i metodi che non vengono definiti in class Rabbit vengono presi “così come sono” da class Animal. Ma se specifichiamo un metodo in Rabbit, come stop() allora verrà utilizzato questo:
class Rabbit extends Animal {
stop() {
// questo verrà utilizzato per rabbit.stop()
// piuttosto di stop() dal padre, class Animal
}
}
Normalmente però non vogliamo rimpiazzare completamente il metodo ereditato, ma piuttosto costruire su di esso, modificarlo leggermente o estendere le sue funzionalità. Nel nostro metodo compiamo delle azioni, ma ad un certo punto richiamiamo il metodo ereditato.
Le classi forniscono la parola chiave “super” per questo scopo.
- super.method(…) per richiamare un metodo dal padre;
- super(…) per richiamare il costruttore del padre (valido solo all’interno del nostro costruttore).
Per esempio, facciamo sì che il nostro coniglio si nasconda automaticamente quando si ferma:
class Animal {
constructor(name) {
this.speed = 0;
this.name = name;
}
run(speed) {
this.speed = speed;
alert(`${this.name} runs with speed ${this.speed}.`);
}
stop() {
this.speed = 0;
alert(`${this.name} stands still.`);
}
}
class Rabbit extends Animal {
hide() {
alert(`${this.name} hides!`);
}
stop() {
super.stop(); // richiama il metodo stop() dal padre
this.hide(); // and then hide
}
}
let rabbit = new Rabbit(“White Rabbit”);
rabbit.run(5); // White Rabbit runs with speed 5.
rabbit.stop(); // White Rabbit stands still. White Rabbit hides!
Ora Rabbit contiene il metodo stop, che richiama al suo interno il metodo super.stop().
SOVRASCRIVERE IL COSTRUTTORE
Sovrascrivere un costruttore è leggermente più complicato. Finora, Rabbit non ha avuto il suo metodo constructor. Secondo le specifiche, se una classe ne estende un’altra e non ha un suo metodo constructor viene generato il seguente constructor “vuoto”:
class Rabbit extends Animal {
// generato per classi figlie senza un costruttore proprio
constructor(…args) {
super(…args);
}
}
Come possiamo vedere, esso richiama il constructor del padre, passandogli tutti gli argomenti. Questo accade se non creiamo un costruttore ad hoc. Aggiungiamo quindi un constructor personalizzato per Rabbit, che specificherà, oltre al name, anche la proprietà earLength:
class Animal {
constructor(name) {
this.speed = 0;
this.name = name;
}
// …
}
class Rabbit extends Animal {
constructor(name, earLength) {
this.speed = 0;
this.name = name;
this.earLength = earLength;
}
// …
}
// Non funziona!
let rabbit = new Rabbit(“White Rabbit”, 10); // Error: this is not defined.
(Errore: “this” non è definito) Ops! Abbiamo ricevuto un errore. Ora non possiamo creare conigli (rabbits). Cosa è andato storto?
La risposta breve è:
- I costruttori nelle classi che ereditano devono chiamare super(…), e bisogna farlo (!) prima di utilizzare this.
Ma perché? Cosa sta succedendo? In effetti, questa richiesta sembra un po’ strana. Ovviamente una spiegazione c’è. Addentriamoci nei dettagli, così da capire cosa effettivamente succede. In JavaScript vi è una netta distinzione tra il “metodo costruttore di una classe figlia” e tutte le altre. In una classe figlia, il costruttore viene etichettato con una proprietà interna speciale: [[ConstructorKind]]:”derived”.
La differenza è:
- Quando viene eseguito un costruttore normale, esso crea un oggetto vuoto chiamato this e continua a lavorare su quello. Questo non avviene quando il costruttore di una classe figlia viene eseguito, dato che si aspetta che il costruttore del padre lo faccia per lui.
Se stiamo creando il costruttore di un figlio dobbiamo per forza richiamare super, altrimenti l’oggetto referenziato da this non verrebbe creato. E riceveremmo un errore. Per far funzionare Rabbit dobbiamo richiamare super() prima di usare this:
class Animal {
constructor(name) {
this.speed = 0;
this.name = name;
}
// …
}
class Rabbit extends Animal {
constructor(name, earLength) {
super(name);
this.earLength = earLength;
}
// …
}
// finalmente
let rabbit = new Rabbit(“White Rabbit”, 10);
alert(rabbit.name); // White Rabbit
alert(rabbit.earLength); // 10
Scrivi un commento