Je kunt niet ver komen in JavaScript zonder met objecten te maken te hebben. Ze vormen de basis van bijna elk aspect van de JavaScript-programmeertaal. Leren hoe je objecten maakt, is waarschijnlijk een van de eerste dingen die je bestudeerde toen je begon. Om zo effectief mogelijk te leren over prototypen in JavaScript, gaan we terug naar de basis en laten we onze innerlijke junior-ontwikkelaar zien.
Objecten zijn sleutel/waarde-paren. De meest gebruikelijke manier om een object te maken is met accolades {}
en je voegt eigenschappen en methoden aan een object toe met behulp van puntnotatie.
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}
Simpel. Nu is de kans groot dat we in onze applicatie meer dan één dier moeten maken. De volgende stap zou natuurlijk zijn om die logica in te kapselen in een functie die we kunnen oproepen als we een nieuw dier willen maken. We noemen dit patroon Functional Instantiation
en we noemen de functie zelf een “constructor functie” omdat het verantwoordelijk is voor het “construeren” van een nieuw object.
Functionele Instantiatie
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
Dat is het. We komen er wel.
Nu hoeven we alleen maar onze Animal
-functie aan te roepen als we een nieuw dier (of, ruimer gezien, een nieuwe “instantie”) willen maken, waarbij we het name
en energy
-niveau van het dier doorgeven. Dit werkt geweldig en het is ongelooflijk eenvoudig. Kun je echter zwakke punten ontdekken in dit patroon? De grootste en degene die we zullen proberen op te lossen heeft te maken met de drie methoden – eat
sleep
, en play
. Elk van deze methodes zijn niet alleen dynamisch, maar ook volledig generiek. Wat dat betekent is dat er geen reden is om deze methodes opnieuw aan te maken zoals we nu doen wanneer we een nieuw dier maken. We verspillen alleen maar geheugen en maken elk dier object groter dan het hoeft te zijn. Kun je een oplossing bedenken? Wat als we in plaats van die methodes telkens opnieuw te creëren als we een nieuw dier maken, we ze naar hun eigen object verplaatsen en dan elk dier naar dat object laten verwijzen? We kunnen dit patroon Functional Instantiation with Shared Methods
noemen, woordelijk maar beschrijvend.
Functionele instantiatie met gedeelde methoden
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)
Door de gedeelde methoden naar hun eigen object te verplaatsen en binnen onze Animal
functie naar dat object te verwijzen, hebben we nu het probleem van geheugenverspilling en te grote dierenobjecten opgelost.
Object.create
Laten we ons voorbeeld nog eens verbeteren door Object.create
te gebruiken. Eenvoudig gezegd kun je met Object.create een object maken dat bij mislukte lookups delegeert naar een ander object. Anders gezegd, Object.create staat je toe om een object te maken en wanneer er een mislukte property lookup is op dat object, kan het een ander object raadplegen om te zien of dat andere object de eigenschap heeft. Dat waren een hoop woorden. Laten we eens wat code bekijken.
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
Dus in het bovenstaande voorbeeld, omdat child
is gemaakt met Object.create(parent)
, zal JavaScript, telkens wanneer er een mislukte property lookup op child
is, die lookup delegeren aan het parent
object. Dat betekent dat ook al heeft child
geen heritage
eigenschap, parent
wel, dus als je child.heritage
logt, krijg je de parent
erfenis die Irish
was.
Nu we Object.create
in ons gereedschapsschuurtje hebben, hoe kunnen we het gebruiken om onze Animal
code van eerder te vereenvoudigen? Wel, in plaats van alle gedeelde methodes één voor één aan het dier toe te voegen zoals we nu doen, kunnen we Object.create gebruiken om te delegeren naar het animalMethods
object in plaats daarvan. Om echt slim te klinken, noemen we deze Functional Instantiation with Shared Methods and Object.create
🙃
Functionele Instantiatie met Gedeelde Methoden en 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)
📈 Dus als we nu leo.eat
aanroepen, zal JavaScript zoeken naar de eat
methode op het leo
object. Dat zoeken zal mislukken, en dan, vanwege Object.create, zal het delegeren naar het animalMethods
object, waar het eat
zal vinden.
Zo ver, zo goed. Er zijn echter nog wat verbeteringen die we kunnen maken. Het lijkt een beetje “hacky” om een apart object (animalMethods
) te moeten beheren om methoden te kunnen delen tussen instanties. Dat lijkt een gebruikelijke functie die je in de taal zelf geïmplementeerd zou willen zien. Dat is het ook en het is de reden waarom je hier bent – prototype
.
Dus wat is prototype
in JavaScript? Nou, simpel gezegd heeft elke functie in JavaScript een prototype
eigenschap die naar een object verwijst. Anticlimactisch, toch?
function doThing () {}console.log(doThing.prototype) // {}
Wat als we in plaats van een apart object te maken om onze methoden te beheren (zoals we doen met animalMethods
), elk van die methoden gewoon op het prototype van de Animal
functie zetten? Dan hoeven we alleen maar Object.create te gebruiken om te delegeren naar animalMethods
, maar kunnen we het gebruiken om te delegeren naar Animal.prototype
. We noemen dit patroon Prototypal Instantiation
.
Prototypal Instantiation
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)
👏👏👏 Hopelijk heb je zojuist een groot “aha”-moment gehad. Nogmaals, prototype
is gewoon een eigenschap die elke functie in JavaScript heeft en, zoals we hierboven zagen, stelt het ons in staat om methoden te delen over alle instanties van een functie. Al onze functionaliteit is nog steeds hetzelfde, maar in plaats van een apart object voor alle methoden te moeten beheren, kunnen we gewoon een ander object gebruiken dat in de Animal
functie zelf is ingebouwd, Animal.prototype
.
Let’s. Go. Dieper.
Op dit punt weten we drie dingen:
- Hoe je een constructorfunctie maakt.
- Hoe je methoden toevoegt aan het prototype van de constructorfunctie.
- Hoe je Object.create gebruikt om mislukte zoekacties te delegeren naar het prototype van de functie.
Die drie taken lijken behoorlijk fundamenteel voor elke programmeertaal. Is JavaScript echt zo slecht dat er geen eenvoudiger, “ingebouwde” manier is om hetzelfde te bereiken? Zoals je nu wel kunt raden, is die er wel, en wel met behulp van het new
sleutelwoord.
Het aardige van de langzame, methodische aanpak die we hebben gevolgd om hier te komen, is dat je nu precies begrijpt wat het new
sleutelwoord in JavaScript onder de motorkap doet.
Als we terugkijken naar onze Animal
constructor, waren de twee belangrijkste onderdelen het maken van het object en het retourneren ervan. Zonder het object te maken met Object.create
, zouden we niet kunnen delegeren naar het prototype van de functie bij mislukte lookups. Zonder het return
statement, zouden we het gemaakte object nooit terugkrijgen.
function Animal (name, energy) { let animal = Object.create(Animal.prototype) animal.name = name animal.energy = energy return animal}
Hier is het leuke van new
– wanneer je een functie aanroept met het new
sleutelwoord, worden deze twee regels impliciet (“onder de motorkap”) voor u gedaan en het object dat wordt gemaakt heet this
.
Door commentaar te gebruiken om te laten zien wat er onder de motorkap gebeurt en ervan uitgaande dat de Animal
constructor wordt aangeroepen met het new
sleutelwoord, kan het als volgt worden herschreven.
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)
en zonder het commentaar “onder de motorkap”
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)
Ook hier is de reden dat dit werkt en dat het this
object voor ons wordt gemaakt, is omdat we de constructorfunctie hebben aangeroepen met het new
sleutelwoord. Als je new
weglaat wanneer je de functie aanroept, wordt dat this
object nooit aangemaakt, noch wordt het impliciet geretourneerd. We zien het probleem in het onderstaande voorbeeld.
function Animal (name, energy) { this.name = name this.energy = energy}const leo = Animal('Leo', 7)console.log(leo) // undefined
De naam voor dit patroon is Pseudoclassical Instantiation
.
Als JavaScript niet je eerste programmeertaal is, word je misschien een beetje onrustig.
“WTF deze kerel heeft net een waardeloosere versie van een Class gemaakt” – Jij
Voor wie er niet mee bekend is: met een Class kun je een blauwdruk voor een object maken. Als je dan een instantie van die Class maakt, krijg je een object met de eigenschappen en methoden die in de blueprint zijn gedefinieerd.
Klinkt dat bekend? Dat is in feite wat we hebben gedaan met onze Animal
constructorfunctie hierboven. Maar in plaats van het sleutelwoord class
te gebruiken, hebben we een gewone JavaScript-functie gebruikt om dezelfde functionaliteit opnieuw te creëren. Toegegeven, het kostte wat extra werk en enige kennis over wat er “onder de motorkap” van JavaScript gebeurt, maar de resultaten zijn hetzelfde.
Hier is het goede nieuws. JavaScript is geen dode taal. Het wordt voortdurend verbeterd en aangevuld door de TC-39 commissie. Dat betekent dat hoewel de eerste versie van JavaScript geen classes ondersteunde, er geen reden is waarom deze niet aan de officiële specificatie kunnen worden toegevoegd. In feite is dat precies wat de TC-39 commissie heeft gedaan. In 2015 werd EcmaScript (de officiële JavaScript-specificatie) 6 uitgebracht met ondersteuning voor klassen en het class
sleutelwoord. Laten we eens kijken hoe onze Animal
constructorfunctie hierboven eruit zou zien met de nieuwe class-syntaxis.
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)
Pretty clean, right?
Dus als dit de nieuwe manier is om classes te maken, waarom hebben we dan zo veel tijd besteed aan het doornemen van de oude manier? De reden daarvoor is dat de nieuwe manier (met het class
keyword) in de eerste plaats slechts “syntactische suiker” is over de bestaande manier die we het pseudo-klassieke patroon hebben genoemd. Om de gemakkelijke syntaxis van ES6 klassen volledig te begrijpen, moet je eerst het pseudo-klassieke patroon begrijpen.
Op dit punt hebben we de grondbeginselen van JavaScript’s prototype behandeld. De rest van dit artikel is gewijd aan het begrijpen van andere “goed om te weten”-onderwerpen die ermee verband houden. In een andere post zullen we bekijken hoe we deze basisprincipes kunnen gebruiken om te begrijpen hoe overerving in JavaScript werkt.
Array Methods
We hebben het hierboven uitgebreid gehad over hoe je, als je methoden wilt delen tussen instanties van een klasse, die methoden in het prototype van de klasse (of functie) moet zetten. We zien hetzelfde patroon als we naar de klasse Array
kijken. In het verleden hebt u waarschijnlijk uw arrays op deze manier gemaakt
const friends =
Het blijkt dat dit gewoon suiker is voor het maken van een new
instantie van de Array
klasse.
const friendsWithSugar = const friendsWithoutSugar = new Array()
Een ding waar je misschien nog nooit over hebt nagedacht, is hoe het komt dat elke instantie van een array al die ingebouwde methoden heeft (splice
slice
pop
, enz)?
Wel, zoals u nu weet, is dat omdat die methoden op Array.prototype
staan en wanneer u een nieuwe instantie van Array
maakt, gebruikt u het new
sleutelwoord dat die delegatie naar Array.prototype
instelt bij mislukte lookups.
We kunnen alle methoden van de array zien door simpelweg Array.prototype
te loggen.
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()*/
Dezelfde logica bestaat ook voor Objecten. Alle objecten delegeren naar Object.prototype
bij mislukte lookups en daarom hebben alle objecten methoden als toString
en hasOwnProperty
.
Static Methods
Tot nu toe hebben we het waarom en hoe van het delen van methoden tussen instanties van een Klasse behandeld. Maar wat als we een methode hadden die belangrijk was voor de Class, maar die niet gedeeld hoefde te worden tussen instanties? Bijvoorbeeld, wat als we een functie hadden die een array van Animal
instanties binnenhaalde en bepaalde welke als volgende moest worden gevoed? We noemen het nextToEat
.
function nextToEat (animals) { const sortedByLeastEnergy = animals.sort((a,b) => { return a.energy - b.energy }) return sortedByLeastEnergy.name}
Het heeft geen zin om nextToEat
op Animal.prototype
te laten staan, omdat we het niet willen delen tussen alle instanties. In plaats daarvan kunnen we het meer zien als een helper-methode. Dus als nextToEat
niet op Animal.prototype
zou moeten staan, waar moeten we het dan plaatsen? Het voor de hand liggende antwoord is dat we nextToEat
in hetzelfde bereik kunnen plaatsen als onze Animal
class en er dan naar kunnen verwijzen als we het nodig hebben zoals we normaal zouden doen.
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
Nu werkt dit, maar er is een betere manier.
Wanneer je een methode hebt die specifiek is voor een klasse zelf, maar niet gedeeld hoeft te worden tussen instanties van die klasse, kun je deze toevoegen als een
static
eigenschap van de klasse.
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 }}
Nu, omdat we nextToEat
als een static
eigenschap aan de klasse hebben toegevoegd, staat deze op de Animal
klasse zelf (niet op het prototype ervan) en kan worden geopend met Animal.nextToEat
.
const leo = new Animal('Leo', 7)const snoop = new Animal('Snoop', 10)console.log(Animal.nextToEat()) // Leo
Omdat we in deze post een soortgelijk patroon hebben gevolgd, bekijken we nu hoe we ditzelfde kunnen bereiken met ES5. In het voorbeeld hierboven zagen we hoe we met behulp van het static
sleutelwoord de methode direct in de klasse zelf zouden plaatsen. Met ES5 is ditzelfde patroon net zo eenvoudig als het handmatig toevoegen van de methode aan het functie-object.
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
Het prototype van een object verkrijgen
Of welk patroon je ook hebt gebruikt om een object te maken, het prototype van dat object kan worden verkregen met de Object.getPrototypeOf
-methode.
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
Er zijn twee belangrijke conclusies te trekken uit de bovenstaande code.
Eerst zult u zien dat proto
een object is met 4 methoden, constructor
eat
sleep
, en play
. Dat is logisch. We hebben getPrototypeOf
gebruikt om de instantie door te geven, leo
om het prototype van die instantie terug te krijgen, waar al onze methoden zich bevinden. Dit vertelt ons nog een ding over prototype
waar we het nog niet over hebben gehad. Standaard zal het prototype
object een constructor
eigenschap hebben die wijst naar de oorspronkelijke functie of de klasse waaruit de instantie is gemaakt. Wat dit ook betekent is dat, omdat JavaScript standaard een constructor
eigenschap op het prototype zet, alle instanties in staat zullen zijn om hun constructor te benaderen via instance.constructor
.
De tweede belangrijke takeaway van hierboven is dat Object.getPrototypeOf(leo) === Animal.prototype
. Dat is ook logisch. De Animal
constructorfunctie heeft een prototype eigenschap waar we methoden kunnen delen over alle instanties en getPrototypeOf
stelt ons in staat om het prototype van de instantie zelf te zien.
function Animal (name, energy) { this.name = name this.energy = energy}const leo = new Animal('Leo', 7)console.log(leo.constructor) // Logs the constructor function
Om aan te sluiten bij wat we eerder hebben besproken met Object.create
, de reden dat dit werkt is dat alle instanties van Animal
zullen delegeren naar Animal.prototype
bij mislukte lookups. Dus wanneer u leo.constructor
probeert te benaderen, heeft leo
geen constructor
eigenschap, dus zal het die lookup delegeren naar Animal.prototype
die inderdaad een constructor
eigenschap heeft. Als deze paragraaf niet duidelijk was, ga dan terug en lees over Object.create
hierboven.
Je hebt misschien al eerder gezien dat __proto__ werd gebruikt om het prototype van een instantie op te halen. Dat is een overblijfsel uit het verleden. Gebruik in plaats daarvan Object.getPrototypeOf(instantie) zoals we hierboven hebben gezien.
Vaststellen of een eigenschap op het prototype staat
Er zijn bepaalde gevallen waarin je moet weten of een eigenschap op de instantie zelf staat of op het prototype waarnaar het object delegeert. We kunnen dit in actie zien door te lussen over ons leo
object dat we hebben gemaakt. Laten we zeggen dat het doel was om te lussen over leo
en al zijn sleutels en waarden te loggen. Als u een for in
-lus gebruikt, zou dat er waarschijnlijk zo uitzien.
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}`)}
Wat zou u verwachten te zien? Waarschijnlijk zoiets als dit –
Key: name. Value: LeoKey: energy. Value: 7
Wat u echter zag als u de code uitvoerde, was dit –
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}
Waarom dat? Nou, een for in
-lus zal alle opsommbare eigenschappen van zowel het object zelf als het prototype waarnaar het delegeert doorlopen. Omdat standaard elke eigenschap die je toevoegt aan het prototype van de functie telbaar is, zien we niet alleen name
en energy
, maar we zien ook alle methodes op het prototype – eat
sleep
, en play
. Om dit op te lossen, moeten we ofwel specificeren dat alle prototype-methodes niet telbaar zijn, of we hebben een manier nodig om alleen console.log te gebruiken als de eigenschap zich op het leo
object zelf bevindt en niet op het prototype waar leo
naar delegeert bij mislukte lookups. Dit is waar hasOwnProperty
ons uit de brand kan helpen.
hasOwnProperty
is een eigenschap op elk object die een boolean teruggeeft die aangeeft of het object de gespecificeerde eigenschap als zijn eigen eigenschap heeft in plaats van op het prototype waarnaar het object delegeert. Dat is precies wat we nodig hebben. Met deze nieuwe kennis kunnen we onze code aanpassen om te profiteren van hasOwnProperty
binnen onze for in
-lus.
...const leo = new Animal('Leo', 7)for(let key in leo) { if (leo.hasOwnProperty(key)) { console.log(`Key: ${key}. Value: ${leo}`) }}
En nu zien we alleen de eigenschappen die op het leo
object zelf staan in plaats van op het prototype leo
waarnaar het ook delegeert.
Key: name. Value: LeoKey: energy. Value: 7
Als u nog steeds een beetje in de war bent over hasOwnProperty
, dan is hier wat code die het misschien opheldert.
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
Controleer of een object een instantie van een klasse is
Soms wil je weten of een object een instantie van een bepaalde klasse is. Om dit te doen kun je de instanceof
operator gebruiken. Het gebruik is vrij eenvoudig, maar de eigenlijke syntax is een beetje vreemd als je het nog nooit gezien hebt. Het werkt als volgt
object instanceof Class
Het bovenstaande statement geeft true terug als object
een instantie is van Class
en false als dat niet het geval is. Als we teruggaan naar ons voorbeeld Animal
, zien we iets als dit.
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
De manier waarop instanceof
werkt, is dat het controleert op de aanwezigheid van constructor.prototype
in de prototypenketen van het object. In het bovenstaande voorbeeld is leo instanceof Animal
omdat true
. Bovendien is leo instanceof User
false
omdat Object.getPrototypeOf(leo) !== User.prototype
.
Het maken van nieuwe agnostische constructorfuncties
Kent u de fout in onderstaande code?
function Animal (name, energy) { this.name = name this.energy = energy}const leo = Animal('Leo', 7)
Zelfs doorgewinterde JavaScript-ontwikkelaars zullen soms struikelen over bovenstaand voorbeeld. Omdat we de pseudoclassical pattern
gebruiken, waar we eerder over hebben geleerd, moeten we, wanneer de Animal
constructorfunctie wordt aangeroepen, ervoor zorgen dat we deze aanroepen met het new
sleutelwoord. Als we dat niet doen, wordt de this
niet aangemaakt en wordt hij ook niet impliciet geretourneerd.
Als opfrisser, de uitgecommentarieerde regels zijn wat er achter de schermen gebeurt als je het new
keyword op een functie gebruikt.
function Animal (name, energy) { // const this = Object.create(Animal.prototype) this.name = name this.energy = energy // return this}
Dit lijkt een te belangrijk detail om aan andere ontwikkelaars over te laten. Ervan uitgaande dat we in een team met andere ontwikkelaars werken, is er een manier waarop we ervoor kunnen zorgen dat onze Animal
constructor altijd wordt aangeroepen met het new
sleutelwoord? Dat blijkt mogelijk te zijn met de instanceof
operator waar we eerder over hebben geleerd.
Als de constructor is aangeroepen met het new
sleutelwoord, dan zal this
binnenin de body van de constructor een instanceof
de constructor functie zelf zijn. Dat waren een hoop grote woorden. Hier is wat code.
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}
Nu in plaats van alleen een waarschuwing aan de gebruiker van de functie te loggen, wat als we de functie opnieuw oproepen, maar deze keer met het new
sleutelwoord?
function Animal (name, energy) { if (this instanceof Animal === false) { return new Animal(name, energy) } this.name = name this.energy = energy}
Nu zal de functie, ongeacht of Animal
wordt aangeroepen met het new
sleutelwoord, nog steeds naar behoren werken.
Hercreëren van Object.create
Doorheen deze post hebben we zwaar vertrouwd op Object.create
om objecten te maken die delegeren naar het prototype van de constructorfunctie. Op dit punt zou u moeten weten hoe u Object.create
in uw code kunt gebruiken, maar één ding waar u misschien nog niet aan hebt gedacht, is hoe Object.create
eigenlijk onder de motorkap werkt. Om u echt te laten begrijpen hoe Object.create
werkt, gaan we het zelf namaken. Eerst, wat weten we over hoe Object.create
werkt?
- Het neemt een argument dat een object is.
- Het maakt een object dat delegeert naar het argument-object bij mislukte lookups.
- Hij retourneert het nieuw aangemaakte object.
Laten we beginnen met #1.
Object.create = function (objToDelegateTo) {}
Eenvoudig genoeg.
Nu #2 – we moeten een object maken dat bij mislukte opzoekingen naar het argument-object delegeert. Dit is een beetje lastiger. Om dit te doen, gebruiken we onze kennis van hoe het new
sleutelwoord en prototypes werken in JavaScript. Eerst maken we in de body van onze Object.create
-implementatie een lege functie. Dan stellen we het prototype van die lege functie gelijk aan het argument object. Dan, om een nieuw object te maken, roepen wij onze lege functie aan met het new
sleutelwoord. Als we dat nieuw gemaakte object retourneren, is #3 ook klaar.
Object.create = function (objToDelegateTo) { function Fn(){} Fn.prototype = objToDelegateTo return new Fn()}
Wild. Laten we er eens doorheen lopen.
Wanneer we een nieuwe functie maken, Fn
in de code hierboven, komt deze met een prototype
eigenschap. Als we die aanroepen met het new
sleutelwoord, weten we dat we een object terugkrijgen dat zal delegeren naar het prototype van de functie bij mislukte lookups. Als we het prototype van de functie overschrijven, dan kunnen we beslissen naar welk object te delegeren bij mislukte opzoekingen. Dus in ons voorbeeld hierboven overschrijven we Fn
’s prototype met het object dat werd doorgegeven toen Object.create
werd aangeroepen, dat we objToDelegateTo
noemen.
Merk op dat we slechts een enkel argument voor Object.create ondersteunen. De officiële implementatie ondersteunt ook een tweede, optioneel argument waarmee je meer eigenschappen aan het gemaakte object kunt toevoegen.
Pijlfuncties
Pijlfuncties hebben geen eigen this
sleutelwoord. Als gevolg daarvan kunnen pijl functies geen constructor functies zijn en als je een pijl functie probeert aan te roepen met het new
sleutelwoord, zal het een foutmelding geven.
const Animal = () => {}const leo = new Animal() // Error: Animal is not a constructor
Ook hebben pijlfuncties geen prototype
eigenschap, omdat we hierboven hebben laten zien dat het pseudo-klassieke patroon niet met pijlfuncties kan worden gebruikt.
const Animal = () => {}console.log(Animal.prototype) // undefined