Lereditariet una caratteristica che permette ad una classe di estendere le propriet di altre classi.
La parola chiave extends
Ipotizziamo di avare 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 loggetto animal e la classe Animal graficamente:
Potremmo voler creare unaltra 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.
La sintassi utilizzate per estendere unaltra classe : class Child extends Parent.
Creiamo 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!
Loggetto 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]] a Animal.prototype:
Ad esempio, per trovare il metodo rabbit.run, il motore JavaScript controlla (dal basso verso lalto in figura):
- Loggetto
rabbit(non possiederun). - Il suo prototype, che
Rabbit.prototype(possiedehide, ma nonrun). - Il suo prototype, che (a causa di
extends)Animal.prototype, che possiede il metodorun.
Come ricordiamo dal capitolo Native prototypes, JavaScript stesso usa lereditariet per prototipi per gli oggetti integrati. E.g. Date.prototype.[[Prototype]] Object.prototype. Questo il motivo per cui le date hanno accesso ai metodi generici di un oggetto.
extendUsare la parola chiave class permette di specificare non solo una classe, ma anche unespressione dopo la parola extends.
Per esempio, una chiamata ad una funzione che genera la classe padre:
``js run function f(phrase) { return class { sayHi() { alert(phrase); } }; }
! class User extends f(Hello) {} /!
new User().sayHi(); // Hello
In questo codice la class User eredita dal risultato della funzione f("Hello").
Questa particolarit pu tornare utile nella programmazione avanzata, quando abbiamo bisogno di generare delle classi padre a seconda di vari parametri. ``
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 cosi 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 allinterno 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().
superCome accennato nel capitolo Arrow functions rivisitate, allinterno delle funzioni a freccia (arrow functions)non si pu utilizzare la parola super
Se acceduto, esso viene preso dalla funzione esterna. Per esempio:
class Rabbit extends Animal {
stop() {
setTimeout(() => super.stop(), 1000); // richiama il metodo stop dal padre dopo 1 secondo
}
}
Il super nella funzione a freccia (arrow function) lo stesso di stop(), quindi funziona come dovrebbe. Se specificassimo una funzione regolare (regular) otterremmo un errore:
// Unexpected super
setTimeout(function() { super.stop() }, 1000);
Sovrascrivere il costruttore
Sovrascrivere un costruttore leggermente pi complicato.
Finora, Rabbit non ha avuto il suo metodoconstructor.
Secondo le specifiche, se una classe ne estende unaltra 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 utilizzarethis.
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
thise 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 loggetto 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
Sovrascrivere i campi di una classe
Questa nota assume che voi abbiate una certa esperienza con le classi, anche in altri linguaggi di programmazione.
Fornisce una spiegazione pi dettagliata del linguaggio e ne illustra il comportamento che potrebbe essere fonte di errori (anche se molto rari).
Se trovate questa sezione troppo difficile da capire, saltatela pure, continuate a leggere, e rileggetela in un secondo momento.
In una sotto-classe possiamo estendere non solo i metodi, ma anche i campi di classe.
Anche se, si verifica un comportamento strano quando proviamo ad accedere ad un campo sovrascritto nel costruttore genitore, piuttosto differente da altri linguaggi di programmazione.
Consideriamo questi esempio:
class Animal {
name = 'animal';
constructor() {
alert(this.name); // (*)
}
}
class Rabbit extends Animal {
name = 'rabbit';
}
new Animal(); // animal
new Rabbit(); // animal
Qui, la classe Rabbit estende Animal e sovrascrive il campo name con il suo valore.
Non c alcun costruttore in Rabbit, quindi viene invocato quello di Animal.
E interessante notare che in entrambi i casi: new Animal() e new Rabbit(), listruzione di alert nella riga (*) mostra animal.
In altre parole, il costruttore genitore utilizza sempre i suoi campi dati, non quelli sovrascritti.
Cosa c di strano in questo?
Se non ancora chiaro, confrontiamo con i metodi.
Qui abbiamo lo stesso codice, ma invece del campo this.name invochiamo il metodo this.showName():
class Animal {
showName() { // invece di this.name = 'animal'
alert('animal');
}
constructor() {
this.showName(); // invece di alert(this.name);
}
}
class Rabbit extends Animal {
showName() {
alert('rabbit');
}
}
new Animal(); // animal
new Rabbit(); // rabbit
Notiamo che loutput differente.
E questo quello che ci aspetteremmo. Quando il costruttore genitore viene invocato da una classe derivata, utilizzate i metodi sovrascritti.
Ma per i campi dati non cosi. Come gi detto, il costruttore genitore utilizza sempre i suoi campi dati.
Perch c questa differenza?
Il motivo sta nellordine di inizializzazione dei campi dati. I campi dati di una classe vengono inizializzati:
- Prima del costruttore per la classe base,
- Subito dopo
super()per le classi derivate.
Nel nostro caso, Rabbit la classe derivata. Non c alcun constructor() al suo interno. Come detto precedentemente, questo equivale ad avere un costruttore vuoto con la sola chiamata a super(...args).
Quindi, new Rabbit() invoca super(), che esegue il costruttore genitore, e (per le regole che segue la classe derivata) solamente dopo vengono inizializzati i suoi campi dati. Al momento dellesecuzione del costruttore genitore, non esiste alcun capo dato in Rabbit, questo il motivo per cui vengono utilizzati i campi dati di Animal.
Abbiamo quindi una sottile differenza di trattamento tra i campi dati ed i metodi in JavaScript.
Fortunatamente, questo comportamento si verifica solamente se un campo dati va a sovrascrivere quelli della classe genitore. Potrebbe essere difficile da capire come comportamento, per questo lo abbiamo spiegato.
Se dovesse verificarsi questo problema, si possono utilizzare i metodi invece dei campi dati.
Super: internamente, [[HomeObject]]
Se state leggendo il tutorial per la prima volta questa sezione pu essere saltata.
Qui spiegheremo i meccanismi interni che stanno dietro lereditariet e super.
Andiamo un p pi a fondo del metodo super. Scopriremo alcune cose interessanti a riguardo.
Beh, proviamo a chiederci, come pu funzionare? Quando un metodo viene eseguito, il suo oggetto di appartenenza viene indicato con this. Se richiamiamo super.method(), dunque, esso dovr recuperare il metodo dal prototipo delloggetto corrente.
Questa attivit pu sembrare semplice, ma non lo . Il motore (engine) conosce loggetto this, quindi potrebbe ottenere il metodo dalla classe padre attraverso this.__proto__.method. Sfortunatamente, una soluzione cos naif non funzioner.
Dimostriamo il problema, usando per semplicit degli oggetti piani (plain objects).
Nellesempio sottostante, rabbit.__proto__ = animal. Ora proviamo: in rabbit.eat() richiamiamo animal.eat() attraverso this.__proto__:
let animal = {
name: "Animal",
eat() {
alert(`${this.name} eats.`);
}
};
let rabbit = {
__proto__: animal,
name: "Rabbit",
eat() {
// super.eat() dovrebbe funzionare presumibilmente cos
this.__proto__.eat.call(this); // (*)
}
};
rabbit.eat(); // Rabbit eats.
Alla linea (*) prendiamo eat dal prototipo (animal) e lo richiamiamo allinterno delloggetto. Nota che .call(this) importante, dato che this.__proto__.eat() richiamerebbe il metodo eat nel contesto della classe padre, non nella classe figlio.
Nellesempio precedente in effetti il metodo funzionava a dovere: abbiamo ricevuto lalert corretto.
Ora proviamo ad aggiungere un altro oggetto. Vedremo cosa non va:
let animal = {
name: "Animal",
eat() {
alert(`${this.name} eats.`);
}
};
let rabbit = {
__proto__: animal,
eat() {
// ...salta in giro come un coniglio e richiama il metodo dalla classe padre (animal)
this.__proto__.eat.call(this); // (*)
}
};
let longEar = {
__proto__: rabbit,
eat() {
// ...fa qualcosa con longEar e richiama il metodo dalla classe padre (rabbit)
this.__proto__.eat.call(this); // (**)
}
};
longEar.eat(); // Error: Maximum call stack size exceeded (Errore: limite massimo di chiamate allo stack superato)
Il codice non funziona pi! Possiamo vedere lerrore provando a richiamare longEar.eat().
Potrebbe non essere cos scontato, ma se tracciamo la chiamata di longEar.eat() possiamo capire perch ci accade. Nelle linee (*)
e (**) il valore di this loggetto corrente (longEar). Questo fondamentale: tutti i metodi di un oggetto ricevono loggetto corrente come this, non attraverso un prototipo o simili.
Quindi, sia nella linea (+) che nella linea (**) il valore di this.__proto__ esattamente lo stesso: rabbit. Entrambi richiamano rabbit.eat senza salire la catena, generando un ciclo (loop) infinito.
Questa immagine rappresenta ci che accade:
- Dentro a
longEar.eat(), la linea(**)richiamarabbit.eatassieme athis=longEar.
// dentro a longEar.eat() abbiamo this = longEar
this.__proto__.eat.call(this) // (**)
// diventa
longEar.__proto__.eat.call(this)
// che uguale a
rabbit.eat.call(this);
```
2. Poi nella linea `(*)` di `rabbit.eat` vorremo passare la chiamata ancora pi in alto nella catena, ma `this=longEar`, dunque `this.__proto__.eat` ancora `rabbit.eat`!
```js
// dentro a rabbit.eat() abbiamo ancora this = longEar
this.__proto__.eat.call(this) // (*)
// diventa
longEar.__proto__.eat.call(this)
// oppure (nuovamente)
rabbit.eat.call(this);
```
3. ...Dunque `rabbit.eat` richiama s stesso in un ciclo (loop) infinito, perch non pu pi salire.
Il problema non pu essere risolto utilizzando solo `this`.
### `[[HomeObject]]`
Per dare una soluzione, JavaScript ha un'altra speciale propriet interna per le funzioni: `[[HomeObject]]`.
Quando una funzione appartiene ad una classe o ad un metodo, la sua propriet `[[HomeObject]]` diventa l'oggetto.
Quindi viene utilizzata da `super` per capire il prototipo del padre e i suoi metodi.
Vediamo come funziona:
```js run
let animal = {
name: "Animal",
eat() { // animal.eat.[[HomeObject]] == animal
alert(`${this.name} eats.`);
}
};
let rabbit = {
__proto__: animal,
name: "Rabbit",
eat() { // rabbit.eat.[[HomeObject]] == rabbit
super.eat();
}
};
let longEar = {
__proto__: rabbit,
name: "Long Ear",
eat() { // longEar.eat.[[HomeObject]] == longEar
super.eat();
}
};
// funziona correttamente
longEar.eat(); // Long Ear eats.
Funziona come dovrebbe, grazie alle meccaniche di [[HomeObject]]. Un metodo, per esempio longEar.eat, conosce il suo [[HomeObject]] e prende il metodo della classe padre da quel prototipo, senza utilizzare this.
I metodi non sono liberi
Come abbiamo gi visto, generalmente le funzioni sono libere, ovvero non sono legate ad un oggetto in JavaScript, cos da poter essere copiate tra gli oggetti ed essere richiamate con un altro this.
Lesistenza di [[HomeObject]] viola questo principio, perch i metodi ricordano i loro oggetti. [[HomeObject]] non pu essere modificato, quindi questo legame dura per sempre.
Lunico posto in cui [[HomeObject]] viene utilizzato in super. Quindi, se un metodo non utilizza super ancora libero e copiabile. Ma con super le cose potrebbero andar male.
Qui di seguito rappresentato un utilizzo sbagliato di super:
let animal = {
sayHi() {
alert(`I'm an animal`);
}
};
// rabbit inherits from animal
let rabbit = {
__proto__: animal,
sayHi() {
super.sayHi();
}
};
let plant = {
sayHi() {
alert("I'm a plant");
}
};
// tree inherits from plant
let tree = {
__proto__: plant,
sayHi: rabbit.sayHi
};
tree.sayHi(); // I'm an animal (?!?)
Una chiamata a tree.sayHi() mostra Im an animal. Completamente sbagliato.
-
Quindi il suo
[[HomeObject]]rabbit, dato che stato creato inrabbit. Non c modo di cambiare[[HomeObject]]; -
Il codice di
tree.sayHi()contienesuper.sayHi(), che va fino arabbite prende il metodo daanimal.
Metodi, non propriet di una funzione
[[HomeObject]] viene definito per metodi appartenenti a classi e ad oggetti piani (plain objects), ma per gli oggetti i metodi vanno definiti come method(), non "method: function()".
La differenza potrebbe non essere rilevante per noi, ma lo per JavaScript.
Nel prossimo esempio, viene utilizzata una sintassi errata (non-method syntax) per fare un confronto. La propriet [[HomeObject]]non viene impostata e lereditariet non funziona:
let animal = {
eat: function() { // dovrebbe corrispondere a eat(){...}
// ...
}
};
let rabbit = {
__proto__: animal,
eat: function() {
super.eat();
}
};
rabbit.eat(); // Errore nella chiamata a super (dato che [[HomeObject non esiste]])
Riepilogo
- Per estendere una classe:
class Child extends Parent:- Questo significa che
Child.prototype.__proto__diventerParent.prototype, quindi i metodi vengono ereditati.
- Questo significa che
- Quando sovrascriviamo un costruttore:
- Dobbiamo richiamare il costruttore del padre attraverso
super()nel costruttore diChildprima di utilizzarethis.
- Dobbiamo richiamare il costruttore del padre attraverso
- Quando sovrascriviamo un metodo:
- Possiamo usare
super.method()in un metodo diChildper richiamare il metodo daParent.
- Possiamo usare
- Meccanismi interni:
- I metodi tengono traccia del loro oggetto o della loro classe nella propriet
[[HomeObject]], cos da poter utilizzaresuperper accedere ai metodi della classe padre. - Non quindi sicuro copiare un metodo in un altro oggetto attraverso
super.
- I metodi tengono traccia del loro oggetto o della loro classe nella propriet
Inoltre:
- Le funzioni a freccia (arrow functions) non hanno un loro
thisosuper, dunque si adattano al contesto in cui si trovano.
Commenti
<code>, per molte righe includile nel tag<pre>, per pi di 10 righe utilizza una sandbox (plnkr, jsbin, codepen)