[ Web Proxy ]
URL:
Viewing: https://it.javascript.info/class-inheritance [Back]  [Original]

Ereditariet delle classi
IT

Vorremo rendere disponibile questo progetto open-source per persone in tutto il mondo.

Aiutaci a tradurre il contenuto di questo tutorial nella tua lingua!

    Cerca in Javascript.info:
    Cerca nel tutorial:
    Light themeDark theme
    DanskEnglishEspaolFranaisIndonesiaItalianoTrkeOzbek

    Ereditariet delle classi

    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):

    1. Loggetto rabbit (non possiede run).
    2. Il suo prototype, che Rabbit.prototype (possiede hide, ma non run).
    3. Il suo prototype, che (a causa di extends) Animal.prototype, che possiede il metodo run.

    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.

    Qualsiasi espressione ammessa dopo extend

    Usare 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().

    Le funzioni a freccia (Arrow functions) non hanno super

    Come 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 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 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

    Nota avanzata

    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]]

    Informazioni avanzate

    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:

    []
    1. Dentro a longEar.eat(), la linea (**) richiama rabbit.eatassieme a this=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 in rabbit. Non c modo di cambiare [[HomeObject]];

    • Il codice di tree.sayHi() contiene super.sayHi(), che va fino a rabbit e prende il metodo da animal.

    []

    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

    1. Per estendere una classe: class Child extends Parent:
      • Questo significa che Child.prototype.__proto__ diventer Parent.prototype, quindi i metodi vengono ereditati.
    2. Quando sovrascriviamo un costruttore:
      • Dobbiamo richiamare il costruttore del padre attraverso super() nel costruttore di Child prima di utilizzare this.
    3. Quando sovrascriviamo un metodo:
      • Possiamo usare super.method() in un metodo di Child per richiamare il metodo da Parent.
    4. Meccanismi interni:
      • I metodi tengono traccia del loro oggetto o della loro classe nella propriet [[HomeObject]], cos da poter utilizzare super per accedere ai metodi della classe padre.
      • Non quindi sicuro copiare un metodo in un altro oggetto attraverso super.

    Inoltre:

    • Le funzioni a freccia (arrow functions) non hanno un loro this o super, dunque si adattano al contesto in cui si trovano.

    Esercizi

    importanza: 5

    Heres the code with Rabbit extending Animal.

    Unfortunately, Rabbit objects cant be created. Whats wrong? Fix it.

    class Animal {
    
      constructor(name) {
        this.name = name;
      }
    
    }
    
    class Rabbit extends Animal {
      constructor(name) {
        this.name = name;
        this.created = Date.now();
      }
    }
    
    let rabbit = new Rabbit("White Rabbit"); // Error: this is not defined
    alert(rabbit.name);
    soluzione

    Thats because the child constructor must call super().

    Heres the corrected code:

    class Animal {
    
      constructor(name) {
        this.name = name;
      }
    
    }
    
    class Rabbit extends Animal {
      constructor(name) {
        super(name);
        this.created = Date.now();
      }
    }
    
    let rabbit = new Rabbit("White Rabbit"); // ok now
    alert(rabbit.name); // White Rabbit
    importanza: 5

    Weve got a Clock class. As of now, it prints the time every second.

    class Clock {
      constructor({ template }) {
        this.template = template;
      }
    
      render() {
        let date = new Date();
    
        let hours = date.getHours();
        if (hours < 10) hours = '0' + hours;
    
        let mins = date.getMinutes();
        if (mins < 10) mins = '0' + mins;
    
        let secs = date.getSeconds();
        if (secs < 10) secs = '0' + secs;
    
        let output = this.template
          .replace('h', hours)
          .replace('m', mins)
          .replace('s', secs);
    
        console.log(output);
      }
    
      stop() {
        clearInterval(this.timer);
      }
    
      start() {
        this.render();
        this.timer = setInterval(() => this.render(), 1000);
      }
    }

    Create a new class ExtendedClock that inherits from Clock and adds the parameter precision the number of ms between ticks. Should be 1000 (1 second) by default.

    • Your code should be in the file extended-clock.js
    • Dont modify the original clock.js. Extend it.

    Apri una sandbox per l'esercizio.

    soluzione
    class ExtendedClock extends Clock {
      constructor(options) {
        super(options);
        let { precision = 1000 } = options;
        this.precision = precision;
      }
    
      start() {
        this.render();
        this.timer = setInterval(() => this.render(), this.precision);
      }
    };

    Apri la soluzione in una sandbox.

    Mappa del tutorial

    Commenti

    leggi questo prima di lasciare un commento
    • Per qualsiasi suggerimento - per favore, apri una issue su GitHub o una pull request, piuttosto di lasciare un commento.
    • Se non riesci a comprendere quanto scitto nell'articolo ti preghiamo di fornire una spiegazione chiara.
    • Per inserire delle righe di codice utilizza il tag <code>, per molte righe includile nel tag <pre>, per pi di 10 righe utilizza una sandbox (plnkr, jsbin, codepen)

    Web Proxy Viewer  |  New URL  |  Original Page