Ogni oggetto ha un prototype
JavaScript è un linguaggio basato sui prototype. Sembra una cosa esotica, ma l'idea è semplice: ogni oggetto ha un collegamento segreto a un altro oggetto, il suo prototype, e quando chiedi una proprietà che l'oggetto non ha, JavaScript segue il collegamento e la cerca lì.
rabbit non ha una proprietà eats sua. JavaScript controlla rabbit, non la trova, segue il collegamento al prototype animal, trova eats: true e la restituisce. Per flies risale la catena, non trova niente e restituisce undefined.
Questo comportamento di ricerca e risalita è l'intero meccanismo. Ereditarietà, metodi, class: tutto è costruito su questo.
La catena dei prototype
La catena non si ferma a un passo. Un prototype può avere a sua volta un prototype, e così via, finché non arrivi a null:
Eseguilo e vedrai rabbit, poi Object.prototype, poi null. Per questo rabbit.toString() funziona anche se non hai mai definito toString: si trova su Object.prototype, la cima di quasi tutte le catene.
La ricerca di una proprietà risale questa catena dal basso verso l'alto. L'assegnazione invece scrive sempre sull'oggetto stesso: non risale mai. Questa asimmetria è importante e sorprende spesso.
Funzioni costruttore e .prototype
Prima che esistesse class, il modo standard per creare tanti oggetti simili era una funzione costruttore chiamata con new:
Quando chiami new User("Ada") succedono due cose:
- Viene creato un oggetto nuovo, con il prototype impostato su
User.prototype. Userviene eseguita conthislegato a quel nuovo oggetto.
greet non viene copiato su ogni istanza. Vive una sola volta su User.prototype, e sia ada sia boris lo trovano risalendo la loro catena. Per questo l'ultima riga stampa true: è letteralmente la stessa funzione.
prototype e __proto__
Questi due nomi confondono tutti. Sono collegati ma non sono la stessa cosa.
User.prototypeè una proprietà della funzione costruttore. È l'oggetto che diventa il prototype delle istanze create connew User(...).ada.__proto__(oppureObject.getPrototypeOf(ada)) è il collegamento sull'istanza che punta al suo prototype.
Nel codice nuovo preferisci Object.getPrototypeOf(obj) a obj.__proto__. __proto__ è un accessor storico mantenuto per compatibilità; la funzione è l'API ufficiale.
Le classi sono zucchero sopra tutto questo
JavaScript moderno ti permette di scrivere class, ma sotto hai sempre a che fare con i prototype. Confronta le due versioni fianco a fianco:
greet è finito su User.prototype, come se l'avessi scritto a mano. La parola chiave class ti dà soprattutto una sintassi più ordinata, regole più rigide (devi usare new) e un modo più pulito di usare extends, ma il modello a runtime è identico.
Saperlo conta quando leggi i messaggi di errore o fai il debug di this. Un errore che parla di "User.prototype.greet" non è uno strano nome interno: è esattamente il posto in cui vive il metodo.
L'ereditarietà è solo una catena più lunga
extends collega un prototype a un altro. Il prototype della classe padre diventa il prototype del prototype della classe figlia:
Cercare rex.eat significa risalire rex → Dog.prototype → Animal.prototype, trovare eat lì e chiamarlo con this ancora legato a rex. È tutto quello che fa extends: prepara la catena al posto tuo.
Creare oggetti direttamente con un prototype
Non ti serve per forza un costruttore. Object.create(proto) crea un nuovo oggetto con il prototype che indichi:
Niente class, niente new, niente funzione costruttore. Due oggetti che condividono un metodo tramite un prototype comune. Questa è la forma più essenziale dell'ereditarietà prototipale: tutto il resto è costruito sopra.
hasOwnProperty: proprietà proprie ed ereditate
Dato che la ricerca risale la catena, "foo" in obj restituisce true anche per le proprietà ereditate. Quando devi distinguere una proprietà che appartiene davvero all'oggetto, usa Object.hasOwn (o il più vecchio hasOwnProperty):
name si trova sull'istanza. greet si trova sul prototype. in le trova entrambe; Object.hasOwn trova solo la prima. Conta ogni volta che iteri con for...in o serializzi un oggetto: di solito vuoi solo le proprietà proprie.
Non modificare i prototype nativi
Dato che Array.prototype è condiviso da ogni array del tuo programma, potresti aggiungerci dei metodi:
// Please don't.
Array.prototype.last = function () {
return this[this.length - 1];
};
[1, 2, 3].last(); // 3
Il problema non è che non funziona: funziona. Il problema è che ogni libreria, ogni dipendenza, ogni versione futura di JavaScript condivide ora quello spazio dei nomi con te. Quando Array.prototype.last arriverà come metodo vero con una semantica leggermente diversa, il tuo codice (o quello di qualcun altro) si romperà in modi subdoli. La vicenda di Array.prototype.flatten / Array.prototype.flat è l'esempio classico.
Tieni le funzioni di supporto come funzioni indipendenti:
Una superficie condivisa in meno con cui scontrarsi.
Il modello mentale
Togli tutto il resto, e i prototype si riducono a tre regole:
- Ogni oggetto ha un collegamento a un prototype (che può essere
null). - Le letture delle proprietà risalgono quella catena; le scritture no.
class,newedextendssono modi per costruire quelle catene senza scrivere tuObject.create.
Tieni a mente queste tre regole e il comportamento di this, instanceof, la risoluzione dei metodi e l'ereditarietà andranno tutti al loro posto.
Prossimo passo: l'event loop
I prototype chiudono il discorso sul modello a oggetti. Il prossimo capitolo passa a qualcosa di completamente diverso: come JavaScript esegue davvero il tuo codice nel tempo. L'event loop è ciò che fa comportare timer, Promise e async/await nel modo in cui si comportano, ed è la base di tutto ciò che è asincrono.
Domande frequenti
Cos'è un prototype in JavaScript?
Ogni oggetto JavaScript ha un collegamento interno a un altro oggetto, chiamato il suo prototype. Quando accedi a una proprietà che non si trova sull'oggetto stesso, JavaScript risale quel collegamento (la catena dei prototype) per cercarla. È grazie a questa catena che i metodi definiti una volta sola vengono condivisi tra molte istanze.
Qual è la differenza tra __proto__ e prototype?
prototype è una proprietà delle funzioni costruttore (e delle classi). È l'oggetto che diventerà il prototype delle istanze create con new. __proto__ (oppure Object.getPrototypeOf(obj)) è il collegamento vero e proprio su un'istanza che punta al suo prototype. Quindi instance.__proto__ === Constructor.prototype.
Le classi JavaScript sono solo zucchero sintattico per i prototype?
In gran parte sì. class Foo { bar() {} } mette bar su Foo.prototype, esattamente come se avessi scritto function Foo(){} e Foo.prototype.bar = function(){}. Le classi aggiungono campi privati, regole più rigide e una sintassi più comoda per extends e super, ma il meccanismo sottostante resta quello dei prototype.
Conviene aggiungere metodi ai prototype nativi come Array.prototype?
Quasi mai. Modificare Array.prototype o Object.prototype influisce su ogni array o oggetto del tuo programma, compresi quelli delle librerie. Può entrare in conflitto con future aggiunte al linguaggio e rompere i cicli for...in. Tieni le tue funzioni di supporto in funzioni o moduli separati.