ereditarieta javascript

Programmazione ad oggetti e ereditarietà in Javascript: una spiegazione esauriente

Parliamo di programmazione ad oggetti e ereditarietà in Javascript.

Questo post è una traduzione autorizzata del post Object-orientation and inheritance in JavaScript: a comprehensive explanation, di Manuel Kiessling. Uno dei migliori post mai letti sull’argomento. Semplice ed esaustivo.

La buona notizia è che in realtà è molto semplice, ma la cattiva notizia è che funziona in modo completamente diverso dai linguaggi orientati agli oggetti come C++, Java, Ruby, Python o PHP, rendendone la comprensione non semplicissima.

Ma non temete, l’affronteremo un passo alla volta.

Blueprints versus finger-pointing

Iniziamo guardando come i “tipici” linguaggi orientati agli oggetti creano oggetti.

Parleremo di un oggetto chiamato myCar. myCar è la nostra rappresentazione bits-and-bytes di una macchina reale incredibilmente semplificata. Potrebbe avere attributi come color e weight, e metodi come drive e honk.

In un’applicazione “reale”, myCar potrebbe essere utilizzata per rappresentare una macchina in un gioco dove è guidata da un giocatore – ma noi ignoreremo completamente il contesto di questo oggetto, perché parleremo della natura e dell’utilizzo di esso in maniera più astratta.

Se voleste usare l’oggetto myCar, diciamo, in Java, dovete prima definire il modello (blueprint) di questo oggetto – questo è quello che in Java e molti altri linguaggi orientati agli oggetti è chiamato classe.

Se volete creare l’oggetto myCar, dovete dire a Java di “costruire un nuovo oggetto dopo aver specificato che il modello è nella classe Car”.

L’oggetto appena creato condivide alcuni aspetti del suo modello. Se chiamate il metodo honk sul vostro oggetto in questo modo:

image

l’interprete Java andrà nella classe di myCar per cercare quale codice eseguire, che è definito nel metodo honk della classe Car.

Una società senza classi

Javascript non ha classi. Ma come in altri linguaggi, ci piacerebbe dire all’interprete che dovrebbe costruire l’oggetto myCar seguendo un certo pattern o schema o modello – sarebbe abbastanza fastidioso creare ogni oggetto macchina da capo, assegnando “manualmente” gli attributi e gli oggetti di cui ha bisogno, ogni volta che lo creiamo.

Se dovessimo creare 30 oggetti macchina basati sulla classe Car in Java, questa relazione oggetto-classe ci fornirebbe 30 macchine che sono capaci di guidare e suonare senza dover scrivere 30 metodi drive e honk.

Come viene ottenuto questo in JavaScript? Invece di una relazione oggetto-classe, c’è una relazione oggetto-oggettto.

Quando in Java chiediamo al nostro myCar di suonare, stiamo dicendo “vai a controllare questa classe, che è il mio modello, per trovare il codice di cui hai bisogno”, in JavaScript invece stiamo dicendo “vai a controllare questo oggetto, che è il mio prototipo, lui ha il codice di cui hai bisogno”.

Costruire oggetti attraverso una relazione oggetto-oggetto è chiamato Programmazione Prototype-based, al contrario della Programmazione Class-based usata in linguaggi più tradizionali come Java.

Sono entrambe implementazioni perfettamente valide del paradigma di programmazione ad oggetti – sono solo due approcci diversi.

Creare oggetti

Tuffiamoci un attimo nel codice, possiamo? Come possiamo impostare il  codice per creare il nostro oggetto myCar, in modo che sia un oggetto di tipo Car che può sia guidare che suonare?

Beh, nel modo più semplice possibile, possiamo creare il nostro oggetto completamente da zero, o ex nihilo se amate i latinismi.

Qualcosa del genere:

image

Questo ci da un oggetto chiamato myCar capace di suonare e guidare:

image

Tuttavia, se dovessimo creare 30 auto in questo modo, finiremmo col definire i metodi honk e drive di ogni singola auto, cosa che, come abbiamo detto, vogliamo evitare.

Nella vita reale, ce si occupiamo di creare, ad esempio, matite, e non vogliamo creare ogni singola matita a mano, dovremmo considerare l’idea di costruire una macchina che crei matite e far si che questa macchina le crei al posto nostro.

Dopotutto, questo è quello che facciamo nei linguaggi class-based come Java – definendo una classe Car, otteniamo il creatore di macchine gratuitamente:

image

creerà l’oggetto myCar per noi basandosi sul modello di Car. La parola chiave new fa tutta la “magia” al posto nostro.

JavaScript, invece, lascia la responsabilità a noi di realizzare un creatore di oggetti. Inoltre, ci lascia piena libertà riguardo il modo con cui vogliamo costruire i nostri oggetti.

Nel caso più semplice, possiamo scrivere una funzione che crea oggetti “semplici” che sono identici al nostro oggetto “ex nihilo”, e che non condividono nessun comportamento – semplicemente vengono generati con lo stesso comportamento copiato dentro ognuno di essi.

Oppure, possiamo scrivere una funziona speciale che non solo crea i nostri oggetti, ma fa anche un po’ di “magia nascosta” collegando l’oggetto creato con il suo creatore. Questo consente una condivisione di comportamenti: le funzioni disponibili nei vari oggetti creati puntano ad una singola implementazione. Se l’implementazione di questa funzione cambia dopo che gli oggetti sono stati creati, il che è possibile in JavaScript, il comportamento di tutti gli oggetti che condividono quella funzione cambierà di conseguenza.

Esaminiamo nel dettaglio tutti i diversi modi di creare oggetti nei dettagli.

Usare una semplice funzione per creare semplici oggetti

Nel nostro primo esempio, abbiamo creato un semplice oggetto myCar dal nulla – possiamo quindi semplicemente includere il codice di creazione in una funzione, che ci da così un semplicissimo object creator:

image

Per brevità, la funzione drive è stata omessa.

Ora possiamo usare questa funzione per la creazione massiccia di macchine:

image

Un lato negativo di questo approccio è l’efficienza: per ogni oggetto myCar creato, una nuova funzione honk viene creata e collegata ad esso – creare 1000 oggetti significherebbe che l’interprete JavaScript deve allocare spazio in memoria per 1000 funzioni, sebbene esse abbiano lo stesso comportamento. Questo comporterà uno spreco non necessario di memoria nell’applicazione

Secondo, questo approccio ci priva di alcune interessanti possibilità. Questi oggetti myCar non condividono niente – sono state realizzate dalla stessa funzione, ma sono completamente indipendenti l’uno dall’altro.

E’ come con le automobili reali e un’azienda di auto: sembrano tutte uguali, ma una volta che lasciano la catena di montaggio, sono completamente indipendenti. Se il produttore decidesse che premere il clacson sulle macchine già prodotte deve generare un suono diverso, tutte le auto dovrebbero essere restituite alla fabbrica e modificate.

Nell’universo virtuale di JavaScript, non siamo legati a questi limiti. Creando oggetti in maniera più sofisticata, siamo in grado di cambiare magicamente il comportamento di tutti gli oggetti creati in un solo colpo.

Usare un costruttore per creare oggetti

In JavaScript, le entità che creano gli oggetti con comportamento condiviso sono funzioni chiamate in modo speciale. Queste funzioni speciali sono i costruttori.

Visto che stiamo per introdurre due nuovi concetti che sono entrambi fondamentali per far funzionare il comportamento degli oggetti condivisi, approcceremo la soluzione in due step.

Nel primo step ricreeremo la soluzione precedente (con una funzione che tira fuori oggetti indipendenti di tipo auto), ma questa volta usando un costruttore:

image

Quando questa funzione viene chiamata usando la parola chiave new, come in questo esempio:

image

restituisce implicitamente un nuovo oggetto con la funzione honk collegata.

Usando le parole chiavi this e new, la creazione esplicita e la restituzione del nuovo oggetto non sono più necessari – esso è creato e restituito “dietro le quinte” (la parola chiave new è quella che crea il nuovo, “invisibile” oggetto, e segretamente lo passa alla funzione Car come variabile this).

Potete pensare a questo meccanismo come se fosse questo pseudo-codice:

image

Come detto, questo è più o meno come la soluzione precedente – non dobbiamo creare ogni singolo oggetto auto manualmente, ma ancora non possiamo modificare il comportamento del metodo honk e avere la modifica automaticamente in tutte le auto create.

Ma abbiamo raggiunto il primo obiettivo. Usando un costruttore, tutti gli oggetti ricevono una proprietà speciale che li collega al proprio costruttore:

image

Tutte le myCars create sono collegate al costruttore Car. Questo è ciò che le rende una classe di oggetti collegati, e non solo un mucchio di oggetti che hanno nome simile e le stesse funzioni.

Ora abbiamo finalmente raggiunto il momento di tornare sul misterioso prototype di cui abbiamo parlato nell’introduzione.

Usare il prototyping per condividere efficientemente comportamenti tra oggetti

Come già detto, mentre nella programmazione class-based la classe è il posto dove inserire le funzioni che gli oggetti condivideranno, nella programmazione prototype-based, il posto dove inserire queste funzioni è l’oggetto che fungerà da prototipo per gli altri oggetti.

Ma dove è l’oggetto che fa da prototipo per il nostro oggetto myCar – noi non l’abbiamo mai creato!

E’ stato creato implicitamente per noi, e assegnato alla proprietà

image

(se ve lo state chiedendo, le funzioni JavaScript sono oggetti che hanno anche proprietà).

Ecco la chiave per condividere funzioni tra gli oggetti: ogni volta che invochiamo una funzione su un oggetto, l’interprete JavaScript cerca di trovare quella funzione nell’oggetto chiamato. Ma se non trova la funzione nell’oggetto stesso, chiede all’oggetto il puntatore al suo prototype, quindi va nel prototype, e chiede la funzione. Se la trova, la esegue.

Questo significa che possiamo creare oggetti myCar senza nessuna funzione, creare la funzione honk nel loro prototype, e finire con tutti gli oggetti myCar che sanno come suonare – perché ogni volta che l’interprete cerca di eseguire la funzione honk su un oggetto myCar, sarà ridirezionato al prototype, ed eseguirà la funzione honk li definita.

Ecco come impostare il tutto:

image

Il nostro costruttore è ora vuoto, perché per le nostre semplici auto, non è necessario altro.

Siccome entrambe le myCars sono state create tramite il costruttore, il loro prototype punta a Car.prototype – eseguire myCar1.honk() significherà eseguire Car.prototype.honk().

Vediamo questo cosa ci permette di fare. In JavaScript, gli oggetti possono essere cambiati a runtime. Questo è possibile anche per i prototype. Che è il motivo per cui possiamo cambiare il comportamento di honk anche dopo che le nostro auto sono state create:

image

Ovviamente, possiamo aggiungere altre funzioni a runtime:

image

Ma potremmo addirittura decidere di trattare una sola delle nostre auto in maniera differente:

image

E’ importante capire cosa accade dietro le quinte di questo esempio. Come abbiamo visto, quando viene chiamata una funzione su un oggetto, l’interprete segue un certo percorso per trovare la vera posizione di quella funzione.

Mentre per myCar1, non c’è ancora una funzione honk nell’oggetto stesso, questo non è più vero per myCar2. Quando l’interprete chiama myCar2.honk, c’è una funzione nell’oggetto myCar2 stesso. Pertanto, l’interprete non seguirà più il percorso attraverso il prototype di myCar2, ma eseguirà invece la funzione dell’oggetto myCar2.

Questa è una delle differenze principali rispetto alla programmazione class-based: mentre gli oggetti sono relativamente “rigidi” ad es. in Java, dove la struttura di un oggetto non può essere cambiata a runtime, in JavaScript, l’approccio prototype-based collega gli oggetti di una certa classe più genericamente tra di loro, il che permette di cambiare la struttura degli oggetti in qualsiasi momento.

Inoltre, notate come condividere funzioni tramite il prototype del costruttore sia molto più efficiente del creare oggetti che hanno le proprie funzioni, anche se identiche. Come detto in precedenza, l’engine non sa che queste funzioni sono identiche, e deve allocare memoria per ogni funzione di ogni oggetto. Questo non è più vero quando condividiamo funzioni tramite un prototype comune – la funzione in questione viene messa in memoria solo una volta, e non conta quanti oggetti myCar saranno creati, essi non hanno le funzioni al loro interno, fanno semplicemente riferimento al proprio costruttore, nel quale prototype viene trovata la funzione.

Per darvi un’idea di cosa comporta questa differenza, ecco un semplice confronto. Il primo esempio crea 1.000.000 di oggetti che hanno la funzione al loro interno:

image

In Google Chrome, questo comporta un heap snapshot di 328MB. Ecco lo stesso esempio, ma ora la funzione è condivisa attraverso il prototype del costruttore:

image

Questa volta, la dimensione del heap snapshot è di soli 17MB, circa il 5% della soluzione non efficiente.

Object-orientation, prototyping ed ereditarietà

Finora non abbiamo parlato di ereditarietà in JavaScript, facciamolo ora.

E’ utile condividere comportamenti tra alcune classi di oggetti, ma ci sono casi dove vorremmo condividere comportamenti tra classi di oggetti diversi ma simili.

Immaginate il nostro mondo virtuale non solo con le auto ma anche con le biciclette. Entrambe si guidano, ma mentre l’auto ha un clacson, la bicicletta ha un campanello.

Il fatto di essere entrambe guidabili, li rende oggetti veicoli, ma il non condividere i metodi honk e ring li distingue.

Potremmo illustrare il loro comportamento condiviso e non e la loro relazione in questo modo:

image

Strutturare questa relazione in un linguaggio class-based come Java è lineare: dovremmo definire una classe Vehicle con un metodo drive, e due classi Car e Bike che estendono entrambe la classe Vehicle, e implementano rispettivamente i metodi honk e ring.

Questo farà si che sia gli oggetti car che bike ereditino il comportamento drive dall’ereditarietà delle loro classi.

Come funziona questo in JavaScript, dove non abbiamo classi ma prototype?

Vediamo prima un esempio e poi analizziamolo. Per mantenere il codice breve, partiamo solo con un’auto che eredità da un veicolo:

image

In JavaScript, l’ereditarietà funziona tramite una catena di prototype.

Il prototype del costruttore Car è impostato a un nuovo oggetto vehicle, che stabilisce il collegamento strutturale che consente all’interprete di cercare metodi negli oggetti genitore.

Il prototype del costruttore Vehicle ha la funzione drive. Ecco cosa accade quando all’oggetto myCar è chiesto di chiamare il metodo drive():

  • l’interprete controlla se nell’oggetto myCar c’è un metodo drive, e non lo trova;
  • l’interprete quindi chiede all’oggetto myCar il suo prototype, che è il prototype del suo costruttore Car;
  • quando controlla Car.prototype, l’interprete vede un oggetto vehicle che ha la funzione honk ma non la funzione drive;
  • allora, l’interprete chiede all’oggetto vehicle il suo prototype, che è il prototype del suo costruttore Vehicle;
  • mentre controlla Vehicle.prototype, l’interprete vede un oggetto che ha una funzione drive – l’interprete ora sa quale codice implementa il comportamento myCar.drive() e lo esegue.

Una società senza classi, rivisitata

Abbiamo appena imparato come emulare l’ereditarietà tradizionale (o classica) in JavaScript. Questo concetto era fondamentale per essere poi dimenticato e lasciato alle spalle, per accettare l’idea che in realtà in JavaScript non c’è un vero bisogno delle classi, e non c’è nemmeno bisogno di emularle – inoltre, ci vuole davvero parecchio codice per esprimere l’idea di “vai a guardare in quell’oggetto, lui ha il codice di cui ha bisogno”, no?

E’ stato Douglas Crockford a pensar ad una soluzione intelligente, che permette agli oggetti di ereditare direttamente l’uno dall’altro, senza il bisogno dell’inutile codice presentato nell’esempio precedente. La soluzione è un componente nativo di JavaScript – è la funzione Object.create(), che funziona così:

image

Ora sappiamo abbastanza da capire cosa sta succedendo. Analizziamo un esempio:

image

Anche se molto più conciso ed espressivo, questo codice esegue esattamente lo stesso comportamento senza la necessità di scrivere costruttori dedicati e collegare funzioni ai loro prototype. Come potete vedere, Object.create() si occupa di tutto, al volo, dietro le quinte. Viene creato un costruttore al volo, nel suo prototype viene settato l’oggetto che farà da modello per il nostro nuovo oggetto, e da questo setup viene infine creato un nuovo oggetto.

Concettualmente, questo è esattamente lo stesso caso dell’esempio precedente dove abbiamo definito che Car.prototype era un new Vehicle().

Ma aspetta! Abbiamo creato le funzioni drive e honk nei nostri oggetti, e non nei loro prototype – questo è poco efficiente!

Beh, in questo caso, non lo è. Vediamo perché:

image

Abbiamo ora creato un totale di 5 oggetti, ma quante volte i metodi drive e honk esistono in memoria? Beh, quante volte sono stati definiti? Solo una – e quindi, questa soluzione è tanto efficiente quanto quella nella quale abbiamo definito l’ereditarietà a mano. Ma guardiamo i numeri:

image

E’ venuto fuori che non è esattamente identico – è risultato un head snapshot di 40MB, quindi c’è un po’ di overhead. In ogni caso, in cambio di codice migliore, ne vale la pena.

Commenti

Chi Sono

Classe ‘84, Salernitano, cresciuto a camille e robottoni giapponesi! Sono un imprenditore digitale, blogger, public speaker e autore.
In segreto mi alleno per diventare Batman

Puoi seguirmi qui

La mia newsletter

Ricevi una volta a settimana una mia selezione di super contenuti sul mondo digital, marketing e business.
Libri, tool, video, post, eventi e tanto altro materiale interessante per il tuo lavoro.
Batman sarebbe fiero di te ;).

Risorse consigliate

Il mio libro

Acquista il mio libro sul Growth Hacking

Il mio corso

Acquista il mio corso sul Growth Hacking

Il mio ebook free

Leggi il mio ebook gratuito sul
Growth Hacking

Ultimi post