Vantaggi dell'eredità prototipale rispetto alla classica?


271

Così alla fine ho smesso di trascinare i piedi per tutti questi anni e ho deciso di imparare JavaScript "correttamente". Uno degli elementi più graffianti del design delle lingue è l'implementazione dell'eredità. Avendo esperienza in Ruby, sono stato davvero felice di vedere chiusure e digitazione dinamica; ma per la mia vita non riesco a capire quali benefici si debbano avere dalle istanze di oggetti usando altre istanze per ereditarietà.



Risposte:


560

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

  1. È semplice.
  2. È potente.
  3. Porta a un codice più piccolo e meno ridondante.
  4. È 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:

  1. Il modello prototipo dell'eredità prototipale.
  2. 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:

  1. Crea un oggetto nuovo di zecca.
  2. 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.createfunzione 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 circlee 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 createfunzione crea un clone di circle, ne assegna un nuovo radiuse 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 newparola chiave associa il thispuntatore all'interno del costruttore a un clone del prototypecostruttore.

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

  1. Un modo per creare un nuovo oggetto (ad es. Letterali di oggetti).
  2. Un modo per estendere un oggetto esistente (ad es Object.create.).

Al contrario, l'eredità classica è molto più complicata. In eredità classica hai:

  1. Classi.
  2. Oggetto.
  3. Interfacce.
  4. Classi astratte.
  5. Classi finali.
  6. Classi di base virtuale.
  7. Costruttori.
  8. 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 NullPointerExceptionin 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:

  1. Variabili private.
  2. 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 :

  1. Delegazione o ereditarietà differenziale
  2. 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 _.extendfa proprio questo.

Naturalmente molti programmatori non lo considerano una vera eredità perché instanceofe isPrototypeOfdicono 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.


33
Mentre capisco da dove vieni e sono d'accordo sul fatto che l'ereditarietà prototipale sia molto utile, penso assumendo che "l'ereditarietà prototipale sia migliore dell'eredità classica" sei già destinato a fallire. Per darti una prospettiva, la mia libreria jTypes è una libreria di eredità classica per JavaScript. Quindi, essendo un ragazzo che si è preso il tempo per farlo, mi siedo ancora qui e dirò che l'eredità prototipale è fantastica e molto utile. Ma è semplicemente uno strumento tra i tanti che ha un programmatore. Ci sono ancora molti svantaggi nell'eredità prototipale.
redline

7
Sono completamente d'accordo con quello che hai detto. Sento che troppi programmatori rifiutano JavaScript per la sua mancanza di eredità classica o lo denunciano come un linguaggio semplice e stupido. In realtà è un concetto estremamente potente, sono d'accordo, e molti programmatori dovrebbero accettarlo e impararlo. Detto questo, penso anche che ci sia un numero significativo di sviluppatori in JavaScript che si oppongono a qualsiasi forma di eredità classica tutti insieme in JavaScript, quando in realtà non hanno alcuna base per i loro argomenti. Entrambi sono ugualmente potenti a sé stanti e altrettanto utili.
redline il

9
Bene, questa è la tua opinione, ma continuerò a non essere d'accordo, e penso che la crescente popolarità di cose come CoffeeScript e TypeScript mostri che esiste una grande comunità di sviluppatori che vorrebbe utilizzare questa funzionalità quando appropriato. Come hai detto, ES6 aggiunge zucchero sintattico, ma non offre ancora l'estensione di jTypes. Per contro, non sono io il responsabile del tuo downvote. Anche se non sono d'accordo con te, non credo che ti dia una cattiva risposta. Sei stato molto accurato.
redline,

25
Stai usando molto clone di parole , il che è semplicemente sbagliato. Object.createsta creando un nuovo oggetto con il prototipo specificato. La scelta delle parole dà l'impressione che il prototipo venga clonato.
Pavel Horal,

7
@Aadit: non c'è davvero bisogno di essere così difensivi. La tua risposta è molto dettagliata e merita i voti. Non stavo suggerendo che "collegato" dovrebbe essere un rimpiazzo per "clone", ma che descrive più appropriatamente la connessione tra un oggetto e il prototipo da cui eredita, sia che affermi la tua definizione del termine "clone" " o no. Cambialo o non cambiarlo, è tutta una tua scelta.
Andy E

42

Consentitemi di rispondere effettivamente alla domanda in linea.

L'eredità del prototipo ha le seguenti virtù:

  1. È più adatto ai linguaggi dinamici perché l'eredità è dinamica quanto l'ambiente in cui si trova. (L'applicabilità a JavaScript dovrebbe essere ovvia qui.) Ciò ti consente di fare le cose rapidamente al volo come personalizzare le classi senza enormi quantità di codice di infrastruttura .
  2. È più semplice implementare uno schema di prototipazione di oggetti rispetto ai classici schemi dicotomici di classe / oggetto.
  3. Elimina la necessità di spigoli vivi complessi attorno al modello di oggetto come "metaclassi" (non ho mai metaclasse che mi è piaciuto ... scusate!) O "autovalori" o simili.

Presenta tuttavia i seguenti svantaggi:

  1. La verifica del tipo di un linguaggio prototipo non è impossibile, ma è molto, molto difficile. La maggior parte del "controllo del tipo" dei linguaggi prototipici è costituito da controlli di tipo "anatra tipizzazione" in fase di esecuzione. Questo non è adatto a tutti gli ambienti.
  2. Allo stesso modo è difficile fare cose come l'ottimizzazione dell'invio di metodi mediante analisi statiche (o spesso anche dinamiche!). Si può (sottolineo: può ) essere molto inefficiente molto facilmente.
  3. Allo stesso modo la creazione di oggetti può essere (e di solito è) molto più lenta in un linguaggio di prototipazione di quanto non possa essere in uno schema dicotomico di classe / oggetto più convenzionale.

Penso che tu possa leggere tra le righe sopra e trovare i vantaggi e gli svantaggi corrispondenti dei tradizionali schemi classe / oggetto. Naturalmente ce ne sono altri in ogni area, quindi lascerò il resto alle altre persone che rispondono.


1
Ehi guarda, una risposta concisa che non fa da fan. Vorrei davvero che questa fosse la risposta migliore per la domanda.
siliconrockstar,

Oggi abbiamo compilatori dinamici Just-in-time in grado di compilare il codice mentre il codice è in esecuzione creando diversi pezzi di codice per ogni sezione. JavaScript è in realtà più veloce di Ruby o Python che usano le classi classiche per questo anche se si usano i prototipi perché è stato fatto molto lavoro per ottimizzarlo.
aoeu256

28

IMO il principale vantaggio dell'eredità prototipale è la sua semplicità.

La natura prototipale del linguaggio può confondere le persone che hanno una formazione classica , ma si scopre che in realtà si tratta di un concetto davvero semplice e potente, eredità differenziale .

Non è necessario effettuare la classificazione , il codice è più piccolo, meno ridondante, gli oggetti ereditano da altri oggetti più generali.

Se pensi prototipicamente noterai presto che non hai bisogno di lezioni ...

L'eredità prototipica sarà molto più popolare nel prossimo futuro, la specifica ECMAScript 5th Edition ha introdotto il Object.createmetodo, che consente di produrre una nuova istanza di oggetto che eredita da un'altra in un modo davvero semplice:

var obj = Object.create(baseInstance);

Questa nuova versione dello standard viene implementata da tutti i fornitori di browser e penso che inizieremo a vedere un'eredità prototipale più pura ...


11
"il tuo codice è più piccolo, meno ridondante ...", perché? Ho dato un'occhiata al link di Wikipedia per "ereditarietà differenziale" e non c'è nulla che supporti queste affermazioni. Perché l'eredità classica si tradurrebbe in un codice più ampio e ridondante?
Noel Abrahams,

4
Esatto, sono d'accordo con Noel. L'eredità prototipale è semplicemente un modo per fare un lavoro, ma questo non lo rende nel modo giusto. Diversi strumenti si esibiranno in modi diversi per diversi lavori. L'eredità prototipica ha il suo posto. È un concetto estremamente potente e semplice. Detto questo, la mancanza di supporto per l'incapsulamento e il polimorfismo reali mette JavaScript in una posizione di svantaggio significativo. Queste metodologie sono in circolazione da molto più tempo rispetto a JavaScript e sono solide nei loro principi. Quindi pensare che il prototipo sia "migliore" è solo una mentalità sbagliata.
redline

1
È possibile simulare l'ereditarietà basata su classi utilizzando l'ereditarietà basata sui prototipi, ma non viceversa. Questo potrebbe essere un buon argomento. Inoltre vedo l'incapsulamento più come una convenzione che come una caratteristica del linguaggio (di solito è possibile interrompere l'incapsulamento tramite la riflessione). Per quanto riguarda il polimorfismo, tutto ciò che guadagni non è dover scrivere alcune semplici condizioni "if" durante il controllo degli argomenti del metodo (e un po 'di velocità se il metodo target viene risolto durante la compilazione). Nessun vero svantaggio JavaScript qui.
Pavel Horal,

I prototipi sono fantastici, IMO. Sto pensando di creare un linguaggio funzionale come Haskell ... ma invece di creare astrazioni baserò tutto su prototipi. Invece di generalizzare la somma e fattoriale per "piegare" dovresti creare il prototipo della funzione somma e sostituire + con * e 0 con 1 per creare il prodotto. Spiegherò "Monads" come prototipi che sostituiscono "then" da promesse / callbacks con flatMap essendo sinonimo di "then". Penso che i prototipi possano aiutare a portare la programmazione funzionale alle masse.
aoeu256,

11

Non c'è davvero molto da scegliere tra i due metodi. L'idea di base da capire è che quando al motore JavaScript viene data una proprietà di un oggetto da leggere, controlla prima l'istanza e se quella proprietà manca, controlla la catena del prototipo. Ecco un esempio che mostra la differenza tra prototipo e classico:

prototipale

var single = { status: "Single" },
    princeWilliam = Object.create(single),
    cliffRichard = Object.create(single);

console.log(Object.keys(princeWilliam).length); // 0
console.log(Object.keys(cliffRichard).length); // 0

// Marriage event occurs
princeWilliam.status = "Married";

console.log(Object.keys(princeWilliam).length); // 1 (New instance property)
console.log(Object.keys(cliffRichard).length); // 0 (Still refers to prototype)

Classica con metodi di istanza (inefficiente perché ogni istanza memorizza la propria proprietà)

function Single() {
    this.status = "Single";
}

var princeWilliam = new Single(),
    cliffRichard = new Single();

console.log(Object.keys(princeWilliam).length); // 1
console.log(Object.keys(cliffRichard).length); // 1

Classica efficiente

function Single() {
}

Single.prototype.status = "Single";

var princeWilliam = new Single(),
    cliffRichard = new Single();

princeWilliam.status = "Married";

console.log(Object.keys(princeWilliam).length); // 1
console.log(Object.keys(cliffRichard).length); // 0
console.log(cliffRichard.status); // "Single"

Come puoi vedere, dato che è possibile manipolare il prototipo di "classi" dichiarate in stile classico, non c'è davvero alcun vantaggio nell'utilizzare l'eredità prototipale. È un sottoinsieme del metodo classico.


2
Guardando altre risposte e risorse su questo argomento, la tua risposta sembrerebbe affermare: "L'eredità prototipale è un sottoinsieme dello zucchero sintattico aggiunto a JavaScript per consentire la comparsa dell'eredità classica". L'OP sembra chiedere i vantaggi dell'ereditarietà prototipale in JS rispetto all'eredità classica in altre lingue piuttosto che confrontare le tecniche di istanza all'interno di JavaScript.
A Fader Darkly,

2

Sviluppo Web: ereditarietà prototipale vs. ereditarietà classica

http://chamnapchhorn.blogspot.com/2009/05/prototypal-inheritance-vs-classical.html

Eredità prototipica classica Vs - StackTranslate.it

Classica Vs eredità prototipale


20
Penso che sia meglio riassumere il contenuto dei collegamenti piuttosto che incollare il collegamento (qualcosa che io stesso ero solito fare), a meno che non sia un altro collegamento SO. Questo perché link / siti si interrompono e perdi la risposta alla domanda e ciò influisce potenzialmente sui risultati della ricerca.
James Westgate,

Il primo link non risponde alla domanda sul perché l' eredità prototipale? Lo descrive semplicemente.
viebel
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.