Ereditarietà Prototipale in JavaScript
Impara l'ereditarietà prototipale in JavaScript: il link nascosto [[Prototype]], __proto__ vs Object.getPrototypeOf/setPrototypeOf, la catena dei prototipi, F.prototype, il shadowing delle proprietà e come si lega this lungo la catena.
In JavaScript, gli oggetti possono ereditare proprietà e metodi da altri oggetti attraverso un meccanismo chiamato ereditarietà prototipale. Invece di copiare funzionalità da una classe (come in Java o C++), ogni object mantiene un link nascosto verso un altro object — il suo prototipo — e JavaScript segue quel link ogni volta che non trova una proprietà sull'object stesso. Questa pagina illustra come funziona quel link nascosto, la differenza tra __proto__ e Object.getPrototypeOf/setPrototypeOf, come viene esplorata la catena dei prototipi, il ruolo di F.prototype per le funzioni costruttore, il shadowing delle proprietà e come si comporta this lungo la catena.
Il link nascosto [[Prototype]]
Ogni object JavaScript ha una proprietà interna nascosta chiamata [[Prototype]]. È o null o un riferimento a un altro object, e quell'object di riferimento è detto prototipo dell'object.
[[Prototype]] è uno slot interno della specifica del linguaggio — non è possibile leggerlo con un normale accesso alle proprietà. Lo si manipola invece attraverso due API pubbliche:
- L'accessor storico
__proto__(un getter/setter esposto daObject.prototype). - I metodi moderni e consigliati
Object.getPrototypeOf(obj)eObject.setPrototypeOf(obj, proto).
Il modo più semplice per impostare un prototipo è usare __proto__ all'interno di un object literal. Qui rendiamo animal il prototipo di rabbit, cosicché rabbit possa leggere le proprietà di animal:
__proto__ vs Object.getPrototypeOf / setPrototypeOf
__proto__ è un getter/setter che da tempo è deprecato per uso generale — è standardizzato solo per compatibilità con i browser. Nel codice reale, si preferiscono i metodi espliciti:
Nota la differenza: __proto__ è un accessor di proprietà, mentre getPrototypeOf/setPrototypeOf sono funzioni. Due regole pratiche:
__proto__non è la stessa cosa di[[Prototype]].__proto__è semplicemente l'accessor che legge/scrive lo slot interno[[Prototype]].- Evita di modificare un prototipo dopo che un object è stato creato.
Object.setPrototypeOfe l'assegnazione diobj.__proto__sono operazioni lente: i motori JavaScript ottimizzano molto gli oggetti il cui prototipo è fisso al momento della creazione. Imposta il prototipo una volta sola, quando costruisci l'object.
La catena dei prototipi
Anche il prototipo di un prototipo può avere un proprio prototipo, formando una catena dei prototipi. Quando si legge obj.prop, JavaScript:
- Cerca
propcome proprietà propria diobj. - Se non la trova, segue
[[Prototype]]e la cerca sul prototipo. - Ripete il passo 2 risalendo la catena finché non trova
propo raggiungenull.
Se la catena termina in null senza trovare la proprietà, il risultato è undefined.
Ci sono due limiti sulla catena: i riferimenti non devono formare un ciclo (JavaScript lancia un errore se si tenta di creare un ciclo), e [[Prototype]] deve essere un object oppure null.
Shadowing delle proprietà: scrittura vs lettura
Il prototipo viene consultato solo per la lettura. Quando si scrive o si elimina una proprietà, l'operazione agisce sempre sull'object stesso, mai sul suo prototipo. Assegnare una proprietà che esiste anche sul prototipo crea una proprietà propria che nasconde (fa shadowing) quella ereditata:
L'eccezione riguarda le proprietà accessor (getter/setter): poiché un setter è una chiamata di funzione, scrivere tramite un setter ereditato esegue quel setter invece di creare una nuova proprietà dati propria.
this è sempre l'object chiamante
Una fonte comune di confusione: indipendentemente da dove un metodo si trova nella catena, this al suo interno è l'object prima del punto quando il metodo è stato chiamato — mai il prototipo su cui è stato definito. I metodi ereditati operano quindi sullo stato proprio dell'object che eredita:
Questo è ciò che rende utili i metodi condivisi su un prototipo: una sola definizione del metodo, ma ogni object memorizza i propri dati.
Funzioni costruttore e F.prototype
Impostare [[Prototype]] manualmente per ogni object è laborioso. Il pattern classico è una funzione costruttore usata con new. Ogni funzione ha una proprietà ordinaria chiamata prototype (scritta F.prototype). Quando si chiama new F(), il [[Prototype]] dell'object appena creato viene impostato su F.prototype.
Tieni distinti F.prototype dal [[Prototype]] di un object: F.prototype è una proprietà ordinaria della funzione costruttore che fornisce il [[Prototype]] agli oggetti creati con new F(). Per impostazione predefinita F.prototype è un object con una singola proprietà non enumerabile constructor che punta di nuovo alla funzione stessa.
Nota: La sintassi moderna
classdi ES6 è zucchero sintattico esattamente su questo meccanismo. Una dichiarazioneclasscrea una funzione costruttore, inserisce i suoi metodi suConstructor.prototypee collega la catena con gli stessi link[[Prototype]]. Vedi Ereditarietà delle classi JavaScript per la forma conextends/super.
Creare oggetti con Object.create
Object.create(proto) costruisce un nuovo object con [[Prototype]] impostato direttamente su proto — senza bisogno di un costruttore. È il modo più esplicito per impostare l'ereditarietà, e accetta un secondo argomento: una mappa di descrittori di proprietà, nello stesso formato usato da Object.defineProperties.
La mappa dei descrittori consente di controllare i flag come writable, enumerable e configurable — vedi Flag e Descrittori di Proprietà JavaScript per il significato di ciascun flag. Gli oggetti creati con Object.create(null) (detti oggetti "molto semplici") sono trattati in Metodi del Prototipo, Oggetti senza __proto__.
Ereditarietà a più livelli
Poiché ogni prototipo può avere il proprio prototipo, è possibile costruire catene di più livelli per modellare relazioni più specifiche:
Ispezionare e iterare la catena
Per verificare se un object si trova da qualche parte nella catena di un altro object, si usa isPrototypeOf. Per separare le proprietà proprie da quelle ereditate si usa hasOwnProperty — nota che for...in percorre l'intera catena (solo proprietà enumerabili), mentre Object.keys restituisce solo le chiavi proprie.
Anche i tipi built-in come array, funzioni e date si affidano a questa catena — i loro metodi vivono su Array.prototype, Function.prototype e così via. Vedi Prototipi Nativi JavaScript per come i built-in sono collegati tra loro.
Riepilogo
- Ogni object ha un
[[Prototype]]nascosto che è un altro object oppurenull. - Si legge
[[Prototype]]conObject.getPrototypeOfe lo si imposta conObject.setPrototypeOf;__proto__è l'accessor legacy e modificare un prototipo dopo la creazione è lento. - Le letture risalgono la catena dei prototipi finché la proprietà viene trovata o la catena termina con
null; le scritture e le eliminazioni agiscono sempre sull'object stesso e possono fare shadowing delle proprietà ereditate. thisall'interno di un metodo è l'object su cui il metodo è stato chiamato, non il prototipo dove è stato definito.new F()imposta il[[Prototype]]del nuovo object suF.prototype; laclassdi ES6 è zucchero sintattico su questo stesso meccanismo — vedi Ereditarietà delle classi JavaScript.