Non si può andare molto lontano in JavaScript senza avere a che fare con gli oggetti. Sono fondamentali per quasi ogni aspetto del linguaggio di programmazione JavaScript. Infatti, imparare a creare oggetti è probabilmente una delle prime cose che avete studiato quando avete iniziato. Detto questo, per imparare nel modo più efficace i prototipi in JavaScript, canalizzeremo il nostro sviluppatore junior interiore e torneremo alle basi.
Gli oggetti sono coppie chiave/valore. Il modo più comune per creare un oggetto è con le parentesi graffe {}
e si aggiungono proprietà e metodi ad un oggetto usando la notazione per punti.
let animal = {}animal.name = 'Leo'animal.energy = 10animal.eat = function (amount) { console.log(`${this.name} is eating.`) this.energy += amount}animal.sleep = function (length) { console.log(`${this.name} is sleeping.`) this.energy += length}animal.play = function (length) { console.log(`${this.name} is playing.`) this.energy -= length}
Semplice. Ora è probabile che nella nostra applicazione avremo bisogno di creare più di un animale. Naturalmente, il passo successivo sarebbe quello di incapsulare questa logica all’interno di una funzione che possiamo invocare ogni volta che abbiamo bisogno di creare un nuovo animale. Chiameremo questo schema Functional Instantiation
e chiameremo la funzione stessa una “funzione costruttrice” poiché è responsabile della “costruzione” di un nuovo oggetto.
Istanziazione funzionale
function Animal (name, energy) { let animal = {} animal.name = name animal.energy = energy animal.eat = function (amount) { console.log(`${this.name} is eating.`) this.energy += amount } animal.sleep = function (length) { console.log(`${this.name} is sleeping.`) this.energy += length } animal.play = function (length) { console.log(`${this.name} is playing.`) this.energy -= length } return animal}const leo = Animal('Leo', 7)const snoop = Animal('Snoop', 10)
"I thought this was an Advanced JavaScript course...?" - Your brain
Si. Ci arriveremo.
Ora, ogni volta che vogliamo creare un nuovo animale (o più in generale una nuova “istanza”), tutto quello che dobbiamo fare è invocare la nostra funzione Animal
, passandole il livello name
e energy
dell’animale. Questo funziona benissimo ed è incredibilmente semplice. Tuttavia, puoi individuare qualche punto debole di questo modello? Il più grande e quello che cercheremo di risolvere ha a che fare con i tre metodi – eat
sleep
, e play
. Ognuno di questi metodi non solo è dinamico, ma è anche completamente generico. Ciò significa che non c’è motivo di ricreare questi metodi come stiamo facendo attualmente ogni volta che creiamo un nuovo animale. Stiamo solo sprecando memoria e rendendo ogni oggetto animale più grande del necessario. Potete pensare ad una soluzione? E se invece di ricreare questi metodi ogni volta che creiamo un nuovo animale, li spostassimo in un proprio oggetto e potessimo fare in modo che ogni animale faccia riferimento a quell’oggetto? Possiamo chiamare questo pattern Functional Instantiation with Shared Methods
, verboso ma descrittivo.
Istanziazione funzionale con metodi condivisi
const animalMethods = { eat(amount) { console.log(`${this.name} is eating.`) this.energy += amount }, sleep(length) { console.log(`${this.name} is sleeping.`) this.energy += length }, play(length) { console.log(`${this.name} is playing.`) this.energy -= length }}function Animal (name, energy) { let animal = {} animal.name = name animal.energy = energy animal.eat = animalMethods.eat animal.sleep = animalMethods.sleep animal.play = animalMethods.play return animal}const leo = Animal('Leo', 7)const snoop = Animal('Snoop', 10)
Spostando i metodi condivisi nel proprio oggetto e referenziando tale oggetto all’interno della nostra funzione Animal
, abbiamo risolto il problema dello spreco di memoria e degli oggetti animali troppo grandi.
Object.create
Miglioriamo ancora una volta il nostro esempio usando Object.create
. In parole povere, Object.create vi permette di creare un oggetto che delegherà ad un altro oggetto su ricerche fallite. In altre parole, Object.create vi permette di creare un oggetto e ogni volta che c’è una ricerca di proprietà fallita su quell’oggetto, può consultare un altro oggetto per vedere se quell’altro oggetto ha la proprietà. Sono state un sacco di parole. Vediamo un po’ di codice.
const parent = { name: 'Stacey', age: 35, heritage: 'Irish'}const child = Object.create(parent)child.name = 'Ryan'child.age = 7console.log(child.name) // Ryanconsole.log(child.age) // 7console.log(child.heritage) // Irish
Così nell’esempio sopra, poiché child
è stato creato con Object.create(parent)
, ogni volta che c’è una ricerca di proprietà fallita su child
, JavaScript delegherà quella ricerca all’oggetto parent
. Ciò significa che anche se child
non ha una proprietà heritage
parent
ce l’ha, quindi quando si registra child.heritage
si ottiene l’eredità di parent
che era Irish
.
Ora con Object.create
nel nostro capanno degli attrezzi, come possiamo usarlo per semplificare il nostro codice Animal
di prima? Bene, invece di aggiungere tutti i metodi condivisi all’animale uno per uno come stiamo facendo ora, possiamo usare Object.create per delegare all’oggetto animalMethods
. Per sembrare davvero intelligenti, chiamiamolo Functional Instantiation with Shared Methods and Object.create
🙃
Istanziazione funzionale con metodi condivisi e Object.create
const animalMethods = { eat(amount) { console.log(`${this.name} is eating.`) this.energy += amount }, sleep(length) { console.log(`${this.name} is sleeping.`) this.energy += length }, play(length) { console.log(`${this.name} is playing.`) this.energy -= length }}function Animal (name, energy) { let animal = Object.create(animalMethods) animal.name = name animal.energy = energy return animal}const leo = Animal('Leo', 7)const snoop = Animal('Snoop', 10)leo.eat(10)snoop.play(5)
📈 Così ora quando chiamiamo leo.eat
, JavaScript cercherà il metodo eat
sull’oggetto leo
. Quella ricerca fallirà, quindi, a causa di Object.create, delegherà all’oggetto animalMethods
che è dove troverà eat
.
Fin qui, tutto bene. Ci sono ancora alcuni miglioramenti che possiamo fare. Sembra un po’ “hacky” dover gestire un oggetto separato (animalMethods
) per condividere i metodi tra le istanze. Sembra una caratteristica comune che si vorrebbe implementare nel linguaggio stesso. Si scopre che lo è ed è l’intera ragione per cui siete qui – prototype
.
Cos’è esattamente prototype
in JavaScript? Beh, semplicemente, ogni funzione in JavaScript ha una proprietà prototype
che fa riferimento ad un oggetto. Anticlimatico, vero? Provate voi stessi.
function doThing () {}console.log(doThing.prototype) // {}
E se invece di creare un oggetto separato per gestire i nostri metodi (come stiamo facendo con animalMethods
), mettessimo semplicemente ognuno di quei metodi sul prototipo della funzione Animal
? Allora tutto quello che dovremmo fare è invece di usare Object.create per delegare a animalMethods
, potremmo usarlo per delegare a Animal.prototype
. Chiameremo questo pattern Prototypal Instantiation
.
Istanziazione prototipale
function Animal (name, energy) { let animal = Object.create(Animal.prototype) animal.name = name animal.energy = energy return animal}Animal.prototype.eat = function (amount) { console.log(`${this.name} is eating.`) this.energy += amount}Animal.prototype.sleep = function (length) { console.log(`${this.name} is sleeping.`) this.energy += length}Animal.prototype.play = function (length) { console.log(`${this.name} is playing.`) this.energy -= length}const leo = Animal('Leo', 7)const snoop = Animal('Snoop', 10)leo.eat(10)snoop.play(5)
👏👏👏👏 Speriamo che tu abbia appena avuto un grande momento “aha”. Di nuovo, prototype
è solo una proprietà che ogni funzione in JavaScript ha e, come abbiamo visto sopra, ci permette di condividere i metodi tra tutte le istanze di una funzione. Tutte le nostre funzionalità sono ancora le stesse, ma ora invece di dover gestire un oggetto separato per tutti i metodi, possiamo semplicemente usare un altro oggetto che viene costruito nella Animal
funzione stessa, Animal.prototype
.
Andiamo. Andare. Deeper.
A questo punto sappiamo tre cose:
- Come creare una funzione costruttore.
- Come aggiungere metodi al prototipo della funzione costruttore.
- Come usare Object.create per delegare le ricerche fallite al prototipo della funzione.
Questi tre compiti sembrano piuttosto fondamentali per qualsiasi linguaggio di programmazione. JavaScript è davvero così male che non c’è un modo più facile, “incorporato” per realizzare la stessa cosa? Come potete probabilmente indovinare a questo punto c’è, ed è usando la parola chiave new
.
Quello che è bello dell’approccio lento e metodico che abbiamo preso per arrivare qui è che ora avrete una profonda comprensione di esattamente quello che la parola chiave new
in JavaScript sta facendo sotto il cappuccio.
Guardando indietro al nostro Animal
costruttore, le due parti più importanti erano la creazione dell’oggetto e la sua restituzione. Senza la creazione dell’oggetto con Object.create
, non saremmo in grado di delegare al prototipo della funzione su ricerche fallite. Senza la dichiarazione return
, non avremmo mai indietro l’oggetto creato.
function Animal (name, energy) { let animal = Object.create(Animal.prototype) animal.name = name animal.energy = energy return animal}
Ecco la cosa bella di new
– quando si invoca una funzione usando la parola chiave new
, quelle due righe sono fatte per voi implicitamente (“sotto il cofano”) e l’oggetto che viene creato si chiama this
.
Usando i commenti per mostrare cosa succede sotto il cofano e assumendo che il Animal
costruttore sia chiamato con la new
parola chiave, può essere riscritto così.
function Animal (name, energy) { // const this = Object.create(Animal.prototype) this.name = name this.energy = energy // return this}const leo = new Animal('Leo', 7)const snoop = new Animal('Snoop', 10)
e senza i commenti “sotto il cappuccio”
function Animal (name, energy) { this.name = name this.energy = energy}Animal.prototype.eat = function (amount) { console.log(`${this.name} is eating.`) this.energy += amount}Animal.prototype.sleep = function (length) { console.log(`${this.name} is sleeping.`) this.energy += length}Animal.prototype.play = function (length) { console.log(`${this.name} is playing.`) this.energy -= length}const leo = new Animal('Leo', 7)const snoop = new Animal('Snoop', 10)
Ancora una volta il motivo per cui questo funziona e che l’oggetto this
viene creato per noi è perché abbiamo chiamato la funzione costruttore con la parola chiave new
. Se tralasciamo new
quando invochiamo la funzione, quell’oggetto this
non viene mai creato né restituito implicitamente. Possiamo vedere il problema nell’esempio qui sotto.
function Animal (name, energy) { this.name = name this.energy = energy}const leo = Animal('Leo', 7)console.log(leo) // undefined
Il nome di questo pattern è Pseudoclassical Instantiation
.
Se JavaScript non è il tuo primo linguaggio di programmazione, potresti essere un po’ irrequieto.
“WTF questo tizio ha appena ricreato una versione più schifosa di una Classe” – Tu
Per chi non lo sapesse, una Classe permette di creare un modello di un oggetto. Poi, ogni volta che si crea un’istanza di quella Classe, si ottiene un oggetto con le proprietà e i metodi definiti nel blueprint.
Suona familiare? Questo è fondamentalmente quello che abbiamo fatto con la nostra funzione Animal
costruttore di cui sopra. Tuttavia, invece di usare la parola chiave class
, abbiamo semplicemente usato una vecchia funzione JavaScript per ricreare la stessa funzionalità. Certo, c’è voluto un po’ di lavoro in più e un po’ di conoscenza di ciò che accade “sotto il cofano” di JavaScript, ma i risultati sono gli stessi.
Ecco la buona notizia. JavaScript non è un linguaggio morto. Viene costantemente migliorato e aggiunto dal comitato TC-39. Ciò significa che anche se la versione iniziale di JavaScript non supportava le classi, non c’è ragione per cui esse non possano essere aggiunte alla specifica ufficiale. Infatti, questo è esattamente ciò che il comitato TC-39 ha fatto. Nel 2015, EcmaScript (la specifica ufficiale di JavaScript) 6 è stato rilasciato con il supporto per le classi e la parola chiave class
. Vediamo come la nostra Animal
funzione costruttore di cui sopra apparirebbe con la nuova sintassi di classe.
class Animal { constructor(name, energy) { this.name = name this.energy = energy } eat(amount) { console.log(`${this.name} is eating.`) this.energy += amount } sleep(length) { console.log(`${this.name} is sleeping.`) this.energy += length } play(length) { console.log(`${this.name} is playing.`) this.energy -= length }}const leo = new Animal('Leo', 7)const snoop = new Animal('Snoop', 10)
Piuttosto pulito, giusto?
Se questo è il nuovo modo di creare le classi, perché abbiamo speso così tanto tempo a rivedere il vecchio modo? Il motivo è che il nuovo modo (con la parola chiave class
) è principalmente solo “zucchero sintattico” sopra il modo esistente che abbiamo chiamato pattern pseudo-classico. Per comprendere appieno la sintassi di convenienza delle classi ES6, dovete prima capire il pattern pseudo-classico.
A questo punto abbiamo coperto i fondamenti del prototipo di JavaScript. Il resto di questo post sarà dedicato alla comprensione di altri argomenti “buoni da sapere” relativi ad esso. In un altro post, vedremo come possiamo prendere questi fondamenti e usarli per capire come funziona l’ereditarietà in JavaScript.
Metodi Array
Abbiamo parlato in modo approfondito sopra di come se si vogliono condividere metodi tra le istanze di una classe, si dovrebbero attaccare questi metodi al prototipo della classe (o della funzione). Possiamo vedere questo stesso schema dimostrato se guardiamo la classe Array
. Storicamente avete probabilmente creato i vostri array in questo modo
const friends =
Si scopre che questo è solo zucchero sulla creazione di un’istanza new
della classe Array
.
const friendsWithSugar = const friendsWithoutSugar = new Array()
Una cosa a cui forse non avete mai pensato è come fa ogni istanza di un array ad avere tutti quei metodi built-in (splice
slice
pop
, etc)?
Bene, come ora sapete, è perché questi metodi vivono su Array.prototype
e quando create una nuova istanza di Array
, usate la parola chiave new
che imposta la delega a Array.prototype
su ricerche fallite.
Possiamo vedere tutti i metodi dell’array semplicemente registrando Array.prototype
.
console.log(Array.prototype)/* concat: ƒn concat() constructor: ƒn Array() copyWithin: ƒn copyWithin() entries: ƒn entries() every: ƒn every() fill: ƒn fill() filter: ƒn filter() find: ƒn find() findIndex: ƒn findIndex() forEach: ƒn forEach() includes: ƒn includes() indexOf: ƒn indexOf() join: ƒn join() keys: ƒn keys() lastIndexOf: ƒn lastIndexOf() length: 0n map: ƒn map() pop: ƒn pop() push: ƒn push() reduce: ƒn reduce() reduceRight: ƒn reduceRight() reverse: ƒn reverse() shift: ƒn shift() slice: ƒn slice() some: ƒn some() sort: ƒn sort() splice: ƒn splice() toLocaleString: ƒn toLocaleString() toString: ƒn toString() unshift: ƒn unshift() values: ƒn values()*/
La stessa logica esiste anche per gli oggetti. Tutti gli oggetti delegheranno a Object.prototype
su ricerche fallite che è il motivo per cui tutti gli oggetti hanno metodi come toString
e hasOwnProperty
.
Metodi statici
Fino a questo punto abbiamo coperto il perché e il come di condividere metodi tra istanze di una Classe. Tuttavia, cosa succede se abbiamo un metodo che è importante per la classe, ma che non ha bisogno di essere condiviso tra le istanze? Per esempio, cosa succederebbe se avessimo una funzione che prende un array di Animal
istanze e determina quale deve essere alimentato successivamente? La chiameremo nextToEat
.
function nextToEat (animals) { const sortedByLeastEnergy = animals.sort((a,b) => { return a.energy - b.energy }) return sortedByLeastEnergy.name}
Non ha senso avere nextToEat
in Animal.prototype
poiché non vogliamo condividerlo tra tutte le istanze. Invece, possiamo pensare ad esso come ad un metodo di aiuto. Quindi se nextToEat
non dovrebbe vivere su Animal.prototype
, dove dovremmo metterlo? Beh, la risposta più ovvia è che potremmo semplicemente infilare nextToEat
nello stesso scope della nostra classe Animal
e fare riferimento ad essa quando ne abbiamo bisogno come faremmo normalmente.
class Animal { constructor(name, energy) { this.name = name this.energy = energy } eat(amount) { console.log(`${this.name} is eating.`) this.energy += amount } sleep(length) { console.log(`${this.name} is sleeping.`) this.energy += length } play(length) { console.log(`${this.name} is playing.`) this.energy -= length }}function nextToEat (animals) { const sortedByLeastEnergy = animals.sort((a,b) => { return a.energy - b.energy }) return sortedByLeastEnergy.name}const leo = new Animal('Leo', 7)const snoop = new Animal('Snoop', 10)console.log(nextToEat()) // Leo
Questo funziona, ma c’è un modo migliore.
Ogni volta che avete un metodo che è specifico di una classe stessa ma che non ha bisogno di essere condiviso tra le istanze di quella classe, potete aggiungerlo come una
static
proprietà della classe.
class Animal { constructor(name, energy) { this.name = name this.energy = energy } eat(amount) { console.log(`${this.name} is eating.`) this.energy += amount } sleep(length) { console.log(`${this.name} is sleeping.`) this.energy += length } play(length) { console.log(`${this.name} is playing.`) this.energy -= length } static nextToEat(animals) { const sortedByLeastEnergy = animals.sort((a,b) => { return a.energy - b.energy }) return sortedByLeastEnergy.name }}
Ora, poiché abbiamo aggiunto nextToEat
come proprietà static
della classe, vive sulla classe Animal
stessa (non sul suo prototipo) e vi si può accedere usando Animal.nextToEat
.
const leo = new Animal('Leo', 7)const snoop = new Animal('Snoop', 10)console.log(Animal.nextToEat()) // Leo
Perché abbiamo seguito uno schema simile in tutto questo post, diamo un’occhiata a come potremmo realizzare questa stessa cosa usando ES5. Nell’esempio precedente abbiamo visto come usando la parola chiave static
metteremo il metodo direttamente nella classe stessa. Con ES5, questo stesso schema è semplice come aggiungere manualmente il metodo all’oggetto funzione.
function Animal (name, energy) { this.name = name this.energy = energy}Animal.prototype.eat = function (amount) { console.log(`${this.name} is eating.`) this.energy += amount}Animal.prototype.sleep = function (length) { console.log(`${this.name} is sleeping.`) this.energy += length}Animal.prototype.play = function (length) { console.log(`${this.name} is playing.`) this.energy -= length}Animal.nextToEat = function (animals) { const sortedByLeastEnergy = animals.sort((a,b) => { return a.energy - b.energy }) return sortedByLeastEnergy.name}const leo = new Animal('Leo', 7)const snoop = new Animal('Snoop', 10)console.log(Animal.nextToEat()) // Leo
Prelevare il prototipo di un oggetto
Indifferentemente da qualsiasi schema usato per creare un oggetto, ottenere il prototipo di quell’oggetto può essere realizzato usando il metodo Object.getPrototypeOf
.
function Animal (name, energy) { this.name = name this.energy = energy}Animal.prototype.eat = function (amount) { console.log(`${this.name} is eating.`) this.energy += amount}Animal.prototype.sleep = function (length) { console.log(`${this.name} is sleeping.`) this.energy += length}Animal.prototype.play = function (length) { console.log(`${this.name} is playing.`) this.energy -= length}const leo = new Animal('Leo', 7)const prototype = Object.getPrototypeOf(leo)console.log(prototype)// {constructor: ƒ, eat: ƒ, sleep: ƒ, play: ƒ}prototype === Animal.prototype // true
Ci sono due cose importanti da prendere dal codice sopra.
Primo, noterete che è un oggetto con 4 metodi, constructor
eat
sleep
e play
. Questo ha senso. Abbiamo usato getPrototypeOf
passando l’istanza, leo
ottenendo indietro il prototipo dell’istanza, che è dove vivono tutti i nostri metodi. Questo ci dice anche un’altra cosa su prototype
di cui non abbiamo ancora parlato. Per default, l’oggetto prototype
avrà una proprietà constructor
che punta alla funzione originale o alla classe da cui è stata creata l’istanza. Ciò significa anche che, poiché JavaScript mette una proprietà constructor
sul prototipo per impostazione predefinita, qualsiasi istanza sarà in grado di accedere al suo costruttore tramite instance.constructor
.
Il secondo importante risultato di cui sopra è che Object.getPrototypeOf(leo) === Animal.prototype
. Anche questo ha senso. La funzione Animal
costruttore ha una proprietà prototipo dove possiamo condividere i metodi tra tutte le istanze e getPrototypeOf
ci permette di vedere il prototipo dell’istanza stessa.
function Animal (name, energy) { this.name = name this.energy = energy}const leo = new Animal('Leo', 7)console.log(leo.constructor) // Logs the constructor function
Per collegare ciò di cui abbiamo parlato prima con Object.create
, il motivo per cui questo funziona è che qualsiasi istanza di Animal
delegherà a Animal.prototype
su ricerche fallite. Così, quando si cerca di accedere a leo.constructor
leo
non ha una proprietà constructor
e delegherà la ricerca a Animal.prototype
che ha effettivamente una proprietà constructor
. Se questo paragrafo non ha senso, tornate indietro e leggete il precedente Object.create
.
Potreste aver visto __proto__ usato prima per ottenere il prototipo di un’istanza. Questa è una reliquia del passato. Invece, usate Object.getPrototypeOf(instance) come abbiamo visto sopra.
Determinare se una proprietà vive sul prototipo
Ci sono alcuni casi in cui avete bisogno di sapere se una proprietà vive sull’istanza stessa o se vive sul prototipo a cui l’oggetto delega. Possiamo vederlo in azione facendo un looping sul nostro oggetto leo
che abbiamo creato. Diciamo che l’obiettivo era quello di fare un ciclo su leo
e registrare tutte le sue chiavi e valori. Usando un ciclo for in
, probabilmente assomiglierebbe a questo.
function Animal (name, energy) { this.name = name this.energy = energy}Animal.prototype.eat = function (amount) { console.log(`${this.name} is eating.`) this.energy += amount}Animal.prototype.sleep = function (length) { console.log(`${this.name} is sleeping.`) this.energy += length}Animal.prototype.play = function (length) { console.log(`${this.name} is playing.`) this.energy -= length}const leo = new Animal('Leo', 7)for(let key in leo) { console.log(`Key: ${key}. Value: ${leo}`)}
Cosa vi aspettereste di vedere? Molto probabilmente, qualcosa di simile a questo –
Key: name. Value: LeoKey: energy. Value: 7
Tuttavia, quello che avete visto se avete eseguito il codice è stato questo –
Key: name. Value: LeoKey: energy. Value: 7Key: eat. Value: function (amount) { console.log(`${this.name} is eating.`) this.energy += amount}Key: sleep. Value: function (length) { console.log(`${this.name} is sleeping.`) this.energy += length}Key: play. Value: function (length) { console.log(`${this.name} is playing.`) this.energy -= length}
Perché? Beh, un for in
loop sta per eseguire un ciclo su tutte le proprietà enumerabili sia sull’oggetto stesso che sul prototipo che delega. Poiché per default ogni proprietà aggiunta al prototipo della funzione è enumerabile, vediamo non solo name
e energy
, ma vediamo anche tutti i metodi sul prototipo – eat
sleep
, e play
. Per risolvere questo problema, dobbiamo specificare che tutti i metodi del prototipo non sono enumerabili o abbiamo bisogno di un modo per console.log solo se la proprietà è sull’oggetto leo
stesso e non sul prototipo che leo
delega su ricerche fallite. È qui che hasOwnProperty
può aiutarci.
hasOwnProperty
è una proprietà su ogni oggetto che restituisce un booleano che indica se l’oggetto ha la proprietà specificata come sua proprietà piuttosto che sul prototipo a cui l’oggetto delega. Questo è esattamente ciò di cui abbiamo bisogno. Ora con questa nuova conoscenza, possiamo modificare il nostro codice per sfruttare hasOwnProperty
all’interno del nostro for in
loop.
...const leo = new Animal('Leo', 7)for(let key in leo) { if (leo.hasOwnProperty(key)) { console.log(`Key: ${key}. Value: ${leo}`) }}
E ora quello che vediamo sono solo le proprietà che sono sull’oggetto leo
stesso piuttosto che sul prototipo leo
delegato anch’esso.
Key: name. Value: LeoKey: energy. Value: 7
Se siete ancora un po’ confusi sul hasOwnProperty
, ecco un po’ di codice che potrebbe chiarirvi.
function Animal (name, energy) { this.name = name this.energy = energy}Animal.prototype.eat = function (amount) { console.log(`${this.name} is eating.`) this.energy += amount}Animal.prototype.sleep = function (length) { console.log(`${this.name} is sleeping.`) this.energy += length}Animal.prototype.play = function (length) { console.log(`${this.name} is playing.`) this.energy -= length}const leo = new Animal('Leo', 7)leo.hasOwnProperty('name') // trueleo.hasOwnProperty('energy') // trueleo.hasOwnProperty('eat') // falseleo.hasOwnProperty('sleep') // falseleo.hasOwnProperty('play') // false
Controlla se un oggetto è un’istanza di una classe
A volte vuoi sapere se un oggetto è un’istanza di una classe specifica. Per farlo, potete usare l’operatore instanceof
. Il caso d’uso è abbastanza semplice, ma la sintassi attuale è un po’ strana se non l’avete mai vista prima. Funziona così
object instanceof Class
La dichiarazione di cui sopra restituirà true se object
è un’istanza di Class
e false se non lo è. Tornando al nostro esempio Animal
avremmo qualcosa del genere.
function Animal (name, energy) { this.name = name this.energy = energy}function User () {}const leo = new Animal('Leo', 7)leo instanceof Animal // trueleo instanceof User // false
Il modo in cui instanceof
funziona è controllare la presenza di constructor.prototype
nella catena dei prototipi dell’oggetto. Nell’esempio sopra, leo instanceof Animal
true
perché Object.getPrototypeOf(leo) === Animal.prototype
. Inoltre, leo instanceof User
false
perché Object.getPrototypeOf(leo) !== User.prototype
.
Creazione di nuove funzioni costruttore agnostiche
Si può individuare l’errore nel codice sottostante?
function Animal (name, energy) { this.name = name this.energy = energy}const leo = Animal('Leo', 7)
Anche gli sviluppatori esperti di JavaScript a volte si imbattono nell’esempio precedente. Poiché stiamo usando il pseudoclassical pattern
che abbiamo imparato prima, quando la funzione Animal
costruttore viene invocata, dobbiamo assicurarci di invocarla con la parola chiave new
. Se non lo facciamo, allora la parola chiave this
non verrà creata e non verrà nemmeno restituita implicitamente.
Come aggiornamento, le linee commentate sono ciò che accade dietro le quinte quando si usa la parola chiave new
su una funzione.
function Animal (name, energy) { // const this = Object.create(Animal.prototype) this.name = name this.energy = energy // return this}
Questo sembra un dettaglio troppo importante per lasciarlo ricordare agli altri sviluppatori. Supponendo di lavorare in un team con altri sviluppatori, c’è un modo per assicurarsi che il nostro Animal
costruttore sia sempre invocato con la new
parola chiave? Si scopre che c’è ed è usando l’operatore instanceof
che abbiamo imparato in precedenza.
Se il costruttore è stato chiamato con la parola chiave new
, allora this
all’interno del corpo del costruttore sarà un instanceof
la funzione costruttore stessa. Sono stati un sacco di paroloni. Ecco un po’ di codice.
function Animal (name, energy) { if (this instanceof Animal === false) { console.warn('Forgot to call Animal with the new keyword') } this.name = name this.energy = energy}
Ora invece di registrare solo un avvertimento al consumatore della funzione, cosa succede se re-invochiamo la funzione ma con la parola chiave new
questa volta?
function Animal (name, energy) { if (this instanceof Animal === false) { return new Animal(name, energy) } this.name = name this.energy = energy}
Ora, indipendentemente dal fatto che Animal
sia invocato con la parola chiave new
, funzionerà ancora correttamente.
Ricreare Object.create
In tutto questo post, abbiamo fatto molto affidamento su Object.create
per creare oggetti che delegano al prototipo della funzione costruttore. A questo punto, dovreste sapere come usare Object.create
all’interno del vostro codice, ma una cosa a cui potreste non aver pensato è come Object.create
funzioni effettivamente sotto il cofano. Per farvi capire veramente come funziona Object.create
, lo ricreeremo noi stessi. Prima di tutto, cosa sappiamo di come funziona Object.create
?
- Prende un argomento che è un oggetto.
- Crea un oggetto che delega all’oggetto argomento su ricerche fallite.
- Ritorna il nuovo oggetto creato.
Partiamo da #1.
Object.create = function (objToDelegateTo) {}
Semplice abbastanza.
Ora #2 – dobbiamo creare un oggetto che deleghi all’oggetto argomento su ricerche fallite. Questo è un po’ più complicato. Per farlo, useremo la nostra conoscenza di come la parola chiave new
e i prototipi funzionano in JavaScript. Per prima cosa, all’interno del corpo della nostra implementazione Object.create
, creeremo una funzione vuota. Poi, imposteremo il prototipo di quella funzione vuota uguale all’oggetto argomento. Poi, per creare un nuovo oggetto, invocheremo la nostra funzione vuota usando la parola chiave new
. Se restituiamo l’oggetto appena creato, anche questo finirà #3.
Object.create = function (objToDelegateTo) { function Fn(){} Fn.prototype = objToDelegateTo return new Fn()}
Wild. Quando creiamo una nuova funzione, Fn
nel codice sopra, essa viene fornita con una proprietà prototype
. Quando la invochiamo con la parola chiave new
, sappiamo che ciò che otterremo indietro è un oggetto che delegherà al prototipo della funzione su ricerche fallite. Se sovrascriviamo il prototipo della funzione, allora possiamo decidere a quale oggetto delegare su ricerche fallite. Quindi nel nostro esempio sopra, sovrascriviamo il prototipo di Fn
con l’oggetto che è stato passato quando Object.create
è stato invocato, che chiamiamo objToDelegateTo
.
Nota che stiamo supportando un solo argomento a Object.create. L’implementazione ufficiale supporta anche un secondo argomento opzionale che permette di aggiungere più proprietà all’oggetto creato.
Funzioni freccia
Le funzioni freccia non hanno una propria this
parola chiave. Di conseguenza, le funzioni freccia non possono essere funzioni costruttrici e se si cerca di invocare una funzione freccia con la parola chiave new
, verrà lanciato un errore.
const Animal = () => {}const leo = new Animal() // Error: Animal is not a constructor
Inoltre, poiché abbiamo dimostrato sopra che il pattern pseudo-classico non può essere usato con le funzioni freccia, le funzioni freccia non hanno nemmeno una proprietà prototype
.
const Animal = () => {}console.log(Animal.prototype) // undefined