So che questa risposta è in ritardo di 3 anni, ma penso davvero che le risposte attuali non forniscano abbastanza informazioni su come l'eredità prototipale sia migliore dell'eredità classica .
Per prima cosa vediamo gli argomenti più comuni dichiarati dai programmatori JavaScript in difesa dell'eredità prototipale (sto prendendo questi argomenti dall'attuale pool di risposte):
- È semplice.
- È potente.
- Porta a un codice più piccolo e meno ridondante.
- È dinamico e quindi è meglio per i linguaggi dinamici.
Ora questi argomenti sono tutti validi, ma nessuno si è preoccupato di spiegare il perché. È come dire a un bambino che studiare la matematica è importante. Certo che lo è, ma al bambino di certo non importa; e non puoi fare un bambino come la matematica dicendo che è importante.
Penso che il problema con l'eredità prototipale sia che è spiegato dal punto di vista di JavaScript. Adoro JavaScript, ma l'eredità prototipale in JavaScript è errata. A differenza dell'eredità classica, ci sono due modelli di eredità prototipale:
- Il modello prototipo dell'eredità prototipale.
- Il modello costruttivo dell'eredità prototipale.
Purtroppo JavaScript utilizza il modello di costruzione dell'eredità prototipale. Questo perché quando è stato creato JavaScript, Brendan Eich (il creatore di JS) voleva che assomigliasse a Java (che ha un'eredità classica):
E lo spingevamo come un fratellino a Java, poiché un linguaggio complementare come Visual Basic era in C ++ nelle famiglie linguistiche di Microsoft in quel momento.
Questo è negativo perché quando le persone usano i costruttori in JavaScript pensano ai costruttori che ereditano da altri costruttori. Questo è sbagliato. In prototipale eredità gli oggetti ereditano da altri oggetti. I costruttori non entrano mai in scena. Questo è ciò che confonde la maggior parte delle persone.
Le persone di lingue come Java, che ha un'eredità classica, diventano ancora più confuse perché sebbene i costruttori assomiglino alle classi, non si comportano come le classi. Come ha affermato Douglas Crockford :
Questa indiretta aveva lo scopo di far sembrare il linguaggio più familiare ai programmatori con formazione classica, ma non è riuscito a farlo, come possiamo vedere dall'opinione molto bassa che i programmatori Java hanno di JavaScript. Il modello di costruzione di JavaScript non ha attirato la folla classica. Ha anche oscurato la vera natura prototipale di JavaScript. Di conseguenza, ci sono pochissimi programmatori che sanno usare la lingua in modo efficace.
Ecco qua. Direttamente dalla bocca del cavallo.
Vera eredità prototipo
L'eredità prototipica riguarda gli oggetti. Gli oggetti ereditano le proprietà da altri oggetti. Questo è tutto quello che c'è da fare. Esistono due modi per creare oggetti usando l'ereditarietà prototipale:
- Crea un oggetto nuovo di zecca.
- Clonare un oggetto esistente ed estenderlo.
Nota: JavaScript offre due modi per clonare un oggetto: delega e concatenazione . D'ora in poi userò la parola "clone" per riferirsi esclusivamente all'eredità tramite delega e la parola "copia" per riferirsi esclusivamente all'eredità tramite concatenazione.
Basta parlare. Vediamo alcuni esempi. Di 'che ho un cerchio di raggio 5
:
var circle = {
radius: 5
};
Possiamo calcolare l'area e la circonferenza del cerchio dal suo raggio:
circle.area = function () {
var radius = this.radius;
return Math.PI * radius * radius;
};
circle.circumference = function () {
return 2 * Math.PI * this.radius;
};
Ora voglio creare un altro cerchio di raggio 10
. Un modo per farlo sarebbe:
var circle2 = {
radius: 10,
area: circle.area,
circumference: circle.circumference
};
Tuttavia JavaScript fornisce un modo migliore: la delega . La Object.create
funzione viene utilizzata per fare questo:
var circle2 = Object.create(circle);
circle2.radius = 10;
È tutto. Hai appena fatto l'eredità prototipale in JavaScript. Non è stato semplice? Prendi un oggetto, lo cloni, cambi tutto ciò di cui hai bisogno, e ciao, ti sei procurato un oggetto nuovo di zecca.
Ora potresti chiedere: "Com'è semplice? Ogni volta che voglio creare una nuova cerchia, devo clonare circle
e assegnare manualmente un raggio". Bene, la soluzione è utilizzare una funzione per eseguire il sollevamento pesante per te:
function createCircle(radius) {
var newCircle = Object.create(circle);
newCircle.radius = radius;
return newCircle;
}
var circle2 = createCircle(10);
In effetti puoi combinare tutto questo in un singolo oggetto letterale come segue:
var circle = {
radius: 5,
create: function (radius) {
var circle = Object.create(this);
circle.radius = radius;
return circle;
},
area: function () {
var radius = this.radius;
return Math.PI * radius * radius;
},
circumference: function () {
return 2 * Math.PI * this.radius;
}
};
var circle2 = circle.create(10);
Ereditarietà prototipale in JavaScript
Se si nota nel programma precedente, la create
funzione crea un clone di circle
, ne assegna un nuovo radius
e quindi lo restituisce. Questo è esattamente ciò che fa un costruttore in JavaScript:
function Circle(radius) {
this.radius = radius;
}
Circle.prototype.area = function () {
var radius = this.radius;
return Math.PI * radius * radius;
};
Circle.prototype.circumference = function () {
return 2 * Math.PI * this.radius;
};
var circle = new Circle(5);
var circle2 = new Circle(10);
Il modello di costruzione in JavaScript è il modello prototipo invertito. Invece di creare un oggetto, crei un costruttore. La new
parola chiave associa il this
puntatore all'interno del costruttore a un clone del prototype
costruttore.
Sembra confuso? È perché il modello di costruzione in JavaScript complica inutilmente le cose. Questo è ciò che la maggior parte dei programmatori trova difficile da capire.
Invece di pensare agli oggetti che ereditano da altri oggetti, pensano ai costruttori che ereditano da altri costruttori e poi diventano completamente confusi.
Ci sono molte altre ragioni per cui il modello di costruzione in JavaScript dovrebbe essere evitato. Puoi leggerli nel mio post sul blog qui: Costruttori contro prototipi
Quindi quali sono i vantaggi dell'eredità prototipale rispetto all'eredità classica? Esaminiamo nuovamente gli argomenti più comuni e spieghiamo perché .
1. L'ereditarietà prototipale è semplice
CMS afferma nella sua risposta:
A mio avviso, il principale vantaggio dell'eredità prototipale è la sua semplicità.
Consideriamo cosa abbiamo appena fatto. Abbiamo creato un oggetto circle
che aveva un raggio di 5
. Quindi lo abbiamo clonato e abbiamo dato al clone un raggio di 10
.
Quindi abbiamo solo due cose per far funzionare l'ereditarietà prototipale:
- Un modo per creare un nuovo oggetto (ad es. Letterali di oggetti).
- Un modo per estendere un oggetto esistente (ad es
Object.create
.).
Al contrario, l'eredità classica è molto più complicata. In eredità classica hai:
- Classi.
- Oggetto.
- Interfacce.
- Classi astratte.
- Classi finali.
- Classi di base virtuale.
- Costruttori.
- Distruttori.
Ti viene l'idea. Il punto è che l'eredità prototipale è più facile da capire, più facile da implementare e più facile da ragionare.
Come dice Steve Yegge nel suo classico post sul blog " Portrait of a N00b ":
I metadati sono qualsiasi tipo di descrizione o modello di qualcos'altro. I commenti nel tuo codice sono solo una descrizione in linguaggio naturale del calcolo. Ciò che rende i metadati dei metadati è che non è strettamente necessario. Se ho un cane con alcuni documenti di razza e perdo i documenti, ho ancora un cane perfettamente valido.
Allo stesso modo le classi sono solo metadati. Le classi non sono strettamente richieste per l'eredità. Tuttavia alcune persone (di solito n00bs) trovano le classi più comode con cui lavorare. Dà loro un falso senso di sicurezza.
Bene, sappiamo anche che i tipi statici sono solo metadati. Sono un tipo di commento specializzato rivolto a due tipi di lettori: programmatori e compilatori. I tipi statici raccontano una storia sul calcolo, presumibilmente per aiutare entrambi i gruppi di lettori a capire l'intento del programma. Ma i tipi statici possono essere eliminati in fase di esecuzione, perché alla fine sono solo commenti stilizzati. Sono come scartoffie di razza: potrebbe rendere un certo tipo di personalità insicura più felice per il loro cane, ma di certo al cane non importa.
Come ho affermato prima, le lezioni danno alla gente un falso senso di sicurezza. Ad esempio ottieni troppi messaggi NullPointerException
in Java anche quando il tuo codice è perfettamente leggibile. Trovo che l'eredità classica di solito interferisca con la programmazione, ma forse è solo Java. Python ha un fantastico sistema di eredità classico.
2. L'ereditarietà prototipale è potente
La maggior parte dei programmatori che provengono da un background classico sostengono che l'eredità classica è più potente dell'eredità prototipale perché ha:
- Variabili private.
- Eredità multipla.
Questa affermazione è falsa. Sappiamo già che JavaScript supporta variabili private tramite chiusure , ma per quanto riguarda l'ereditarietà multipla? Gli oggetti in JavaScript hanno solo un prototipo.
La verità è che l'eredità prototipale supporta l'ereditarietà da più prototipi. Eredità prototipica significa semplicemente un oggetto che eredita da un altro oggetto. Esistono due modi per implementare l'ereditarietà prototipale :
- Delegazione o ereditarietà differenziale
- Clonazione o ereditarietà concatenativa
Sì JavaScript consente solo agli oggetti di delegare a un altro oggetto. Tuttavia, consente di copiare le proprietà di un numero arbitrario di oggetti. Ad esempio _.extend
fa proprio questo.
Naturalmente molti programmatori non lo considerano una vera eredità perché instanceof
e isPrototypeOf
dicono il contrario. Tuttavia, questo può essere facilmente risolto memorizzando una serie di prototipi su ogni oggetto che eredita da un prototipo tramite concatenazione:
function copyOf(object, prototype) {
var prototypes = object.prototypes;
var prototypeOf = Object.isPrototypeOf;
return prototypes.indexOf(prototype) >= 0 ||
prototypes.some(prototypeOf, prototype);
}
Quindi l'eredità prototipale è altrettanto potente dell'eredità classica. In effetti è molto più potente dell'eredità classica perché nell'eredità prototipale è possibile selezionare manualmente quali proprietà copiare e quali proprietà omettere da diversi prototipi.
Nell'eredità classica è impossibile (o almeno molto difficile) scegliere quali proprietà si desidera ereditare. Usano le classi di base virtuali e le interfacce per risolvere il problema del diamante .
In JavaScript, tuttavia, molto probabilmente non sentirai mai il problema del diamante perché puoi controllare esattamente quali proprietà desideri ereditare e da quali prototipi.
3. L'ereditarietà prototipale è meno ridondante
Questo punto è un po 'più difficile da spiegare perché l'eredità classica non porta necessariamente a codice più ridondante. Infatti, l'ereditarietà, classica o prototipale, viene utilizzata per ridurre la ridondanza nel codice.
Un argomento potrebbe essere che la maggior parte dei linguaggi di programmazione con eredità classica sono tipizzati staticamente e richiedono all'utente di dichiarare esplicitamente i tipi (a differenza di Haskell che ha una tipizzazione statica implicita). Quindi questo porta a un codice più dettagliato.
Java è noto per questo comportamento. Ricordo distintamente che Bob Nystrom menzionava il seguente aneddoto nel suo post sul blog su Pratt Parsers :
Devi amare il livello di burocrazia di Java "per favore firmalo in quadruplicato" qui.
Ancora una volta, penso che sia solo perché Java fa schifo così tanto.
Un argomento valido è che non tutte le lingue che hanno eredità classica supportano l'ereditarietà multipla. Ancora una volta viene in mente Java. Sì, Java ha interfacce, ma non è sufficiente. A volte hai davvero bisogno di ereditarietà multipla.
Poiché l'ereditarietà prototipica consente l'ereditarietà multipla, il codice che richiede l'ereditarietà multipla è meno ridondante se scritto usando l'ereditarietà prototipale piuttosto che in un linguaggio con eredità classica ma senza eredità multipla.
4. L'ereditarietà prototipale è dinamica
Uno dei vantaggi più importanti dell'ereditarietà dei prototipi è che è possibile aggiungere nuove proprietà ai prototipi dopo che sono stati creati. Ciò consente di aggiungere nuovi metodi a un prototipo che saranno automaticamente resi disponibili a tutti gli oggetti che delegano a quel prototipo.
Questo non è possibile nell'eredità classica perché una volta creata una classe non è possibile modificarla in fase di esecuzione. Questo è probabilmente il più grande vantaggio dell'eredità prototipale rispetto all'eredità classica e avrebbe dovuto essere al vertice. Comunque mi piace salvare il meglio per la fine.
Conclusione
L'eredità prototipica conta. È importante educare i programmatori JavaScript sul perché abbandonare il modello costruttivo dell'eredità prototipale a favore del modello prototipo dell'eredità prototipale.
Dobbiamo iniziare a insegnare JavaScript correttamente e questo significa mostrare ai nuovi programmatori come scrivere codice usando il modello prototipo invece del modello costruttore.
Non solo sarà più facile spiegare l'eredità prototipale usando il modello prototipo, ma renderà anche programmatori migliori.
Se ti è piaciuta questa risposta, dovresti anche leggere il mio post sul blog " Perché le questioni relative all'ereditarietà prototipica ". Fidati di me, non rimarrai deluso.