Sie kommen in JavaScript nicht sehr weit, ohne mit Objekten zu arbeiten. Sie sind die Grundlage für fast jeden Aspekt der JavaScript-Programmiersprache. In der Tat ist das Erlernen des Erstellens von Objekten wahrscheinlich eines der ersten Dinge, die Sie gelernt haben, als Sie anfingen. Um also möglichst effektiv über Prototypen in JavaScript zu lernen, werden wir unseren inneren Jungentwickler kanalisieren und zu den Grundlagen zurückkehren.
Objekte sind Schlüssel/Wert-Paare. Die gebräuchlichste Art, ein Objekt zu erstellen, ist mit geschweiften Klammern {}
und Sie fügen Eigenschaften und Methoden zu einem Objekt hinzu, indem Sie die Punktnotation verwenden.
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}
Einfach. Nun werden wir in unserer Anwendung wahrscheinlich mehr als ein Tier anlegen müssen. Der nächste Schritt wäre dann natürlich, diese Logik in einer Funktion zu kapseln, die wir immer dann aufrufen können, wenn wir ein neues Tier erstellen müssen. Wir nennen dieses Muster Functional Instantiation
und die Funktion selbst nennen wir eine „Konstruktor-Funktion“, da sie für die „Konstruktion“ eines neuen Objekts verantwortlich ist.
Funktionale Instanziierung
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
Das ist es. Wir werden es schaffen.
Jetzt müssen wir, wann immer wir ein neues Tier (oder allgemeiner gesagt eine neue „Instanz“) erstellen wollen, nur noch unsere Animal
-Funktion aufrufen und ihr die name
– und energy
-Ebene des Tieres übergeben. Das funktioniert super und ist unglaublich einfach. Können Sie jedoch irgendwelche Schwachstellen bei diesem Muster erkennen? Die größte und die, die wir versuchen werden zu lösen, hat mit den drei Methoden zu tun – eat
sleep
, und play
. Jede dieser Methoden ist nicht nur dynamisch, sondern auch komplett generisch. Das bedeutet, dass es keinen Grund gibt, diese Methoden neu zu erstellen, wie wir es derzeit tun, wenn wir ein neues Tier erstellen. Wir verschwenden damit nur Speicher und machen jedes Tierobjekt größer als es sein muss. Können Sie sich eine Lösung vorstellen? Wie wäre es, wenn wir diese Methoden nicht jedes Mal, wenn wir ein neues Tier erstellen, neu erstellen, sondern sie in ein eigenes Objekt verschieben und dann jedes Tier auf dieses Objekt verweisen lassen? Wir können dieses Muster Functional Instantiation with Shared Methods
nennen, wortreich aber anschaulich.
Funktionale Instanziierung mit gemeinsamen 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)
Indem wir die gemeinsamen Methoden in ein eigenes Objekt verschieben und dieses Objekt innerhalb unserer Animal
-Funktion referenzieren, haben wir nun das Problem der Speicherverschwendung und der zu großen Tierobjekte gelöst.
Object.create
Lassen Sie uns unser Beispiel noch einmal verbessern, indem wir Object.create
verwenden. Einfach ausgedrückt, können Sie mit Object.create ein Objekt erstellen, das bei fehlgeschlagenen Suchvorgängen an ein anderes Objekt delegiert wird. Anders ausgedrückt: Mit Object.create können Sie ein Objekt erstellen, das bei einer fehlgeschlagenen Suche nach einer Eigenschaft ein anderes Objekt konsultieren kann, um zu sehen, ob dieses andere Objekt die Eigenschaft besitzt. Das waren eine Menge Worte. Schauen wir uns etwas Code an.
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
So wurde im obigen Beispiel, weil child
mit Object.create(parent)
erstellt, wird JavaScript bei jeder fehlgeschlagenen Property-Suche auf child
diese Suche an das Objekt parent
delegieren. Das bedeutet, dass, obwohl child
keine heritage
-Eigenschaft hat, parent
hat eine, so dass Sie beim Loggen von child.heritage
das Erbe von parent
erhalten, das Irish
war.
Nun mit Object.create
in unserem Werkzeugschuppen, wie können wir es verwenden, um unseren Animal
Code von früher zu vereinfachen? Nun, anstatt alle gemeinsam genutzten Methoden eine nach der anderen zu dem Tier hinzuzufügen, wie wir es jetzt tun, können wir stattdessen Object.create verwenden, um an das animalMethods
-Objekt zu delegieren. Um wirklich schlau zu klingen, nennen wir dieses Functional Instantiation with Shared Methods and Object.create
🙃
Funktionale Instanziierung mit gemeinsamen Methoden und 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)
📈 Wenn wir nun also leo.eat
aufrufen, sucht JavaScript nach der eat
-Methode auf dem leo
-Objekt. Diese Suche wird fehlschlagen, dann wird es aufgrund von Object.create an das animalMethods
-Objekt delegieren, wo es eat
findet.
So weit, so gut. Es gibt aber noch einige Verbesserungen, die wir machen können. Es scheint ein wenig „hakelig“, ein separates Objekt (animalMethods
) verwalten zu müssen, um Methoden über Instanzen hinweg zu teilen. Das scheint eine übliche Funktion zu sein, die man in der Sprache selbst implementiert haben möchte. Wie sich herausstellt, ist es das auch und es ist der Grund, warum Sie hier sind – prototype
.
Was genau ist also prototype
in JavaScript? Nun, einfach gesagt, jede Funktion in JavaScript hat eine prototype
-Eigenschaft, die ein Objekt referenziert. Antiklimaktisch, oder? Testen Sie es selbst.
function doThing () {}console.log(doThing.prototype) // {}
Wie wäre es, wenn wir, anstatt ein separates Objekt zu erstellen, um unsere Methoden zu verwalten (wie wir es mit animalMethods
tun), einfach jede dieser Methoden auf den Animal
Funktionsprototyp legen? Dann müssten wir nur noch, statt Object.create zu verwenden, um an animalMethods
zu delegieren, könnten wir es verwenden, um an Animal.prototype
zu delegieren. Wir nennen dieses Muster Prototypal Instantiation
.
Prototypische Instantiierung
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)
👏👏👏 Hoffentlich hatten Sie gerade einen großen „Aha“-Moment. Noch einmal: prototype
ist einfach eine Eigenschaft, die jede Funktion in JavaScript hat, und wie wir oben gesehen haben, erlaubt sie uns, Methoden über alle Instanzen einer Funktion hinweg zu teilen. Die gesamte Funktionalität ist immer noch dieselbe, aber statt ein separates Objekt für alle Methoden zu verwalten, können wir jetzt einfach ein anderes Objekt verwenden, das in die Animal
-Funktion selbst eingebaut ist, Animal.prototype
.
Let’s. Go. Deeper.
Zu diesem Zeitpunkt wissen wir drei Dinge:
- Wie man eine Konstruktorfunktion erstellt.
- Wie man Methoden zum Prototyp der Konstruktorfunktion hinzufügt.
- Wie man Object.create verwendet, um fehlgeschlagene Suchvorgänge an den Prototyp der Funktion zu delegieren.
Diese drei Aufgaben scheinen für jede Programmiersprache ziemlich grundlegend zu sein. Ist JavaScript wirklich so schlecht, dass es keinen einfacheren, „eingebauten“ Weg gibt, das Gleiche zu erreichen? Wie Sie sich wahrscheinlich denken können, gibt es einen, und zwar das Schlüsselwort new
.
Was das Schöne an der langsamen, methodischen Herangehensweise ist, ist, dass Sie jetzt ein tiefes Verständnis dafür haben, was das Schlüsselwort new
in JavaScript unter der Haube tut.
Bei der Betrachtung unseres Animal
-Konstruktors waren die beiden wichtigsten Teile das Erzeugen des Objekts und die Rückgabe des Objekts. Ohne das Erzeugen des Objekts mit Object.create
wären wir nicht in der Lage, bei fehlgeschlagenen Lookups an den Prototyp der Funktion zu delegieren. Ohne die return
-Anweisung würden wir das erstellte Objekt nie zurückbekommen.
function Animal (name, energy) { let animal = Object.create(Animal.prototype) animal.name = name animal.energy = energy return animal}
Hier ist das Coole an new
– wenn Sie eine Funktion mit dem Schlüsselwort new
aufrufen, Wenn Sie eine Funktion mit dem Schlüsselwort new
aufrufen, werden diese beiden Zeilen implizit („unter der Haube“) für Sie erledigt und das Objekt, das erstellt wird, heißt this
.
Anhand von Kommentaren, die zeigen, was unter der Haube passiert, und unter der Annahme, dass der Animal
-Konstruktor mit dem new
-Schlüsselwort aufgerufen wird, kann er wie folgt umgeschrieben werden.
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)
und ohne die „unter der Haube“-Kommentare
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)
Auch hier ist der Grund, warum das funktioniert und dass das this
-Objekt für uns erzeugt wird, ist, dass wir die Konstruktorfunktion mit dem Schlüsselwort new
aufgerufen haben. Wenn Sie new
beim Aufruf der Funktion weglassen, wird das this
-Objekt weder erstellt noch implizit zurückgegeben. Wir können das Problem damit im folgenden Beispiel sehen.
function Animal (name, energy) { this.name = name this.energy = energy}const leo = Animal('Leo', 7)console.log(leo) // undefined
Der Name für dieses Muster ist Pseudoclassical Instantiation
.
Wenn JavaScript nicht Ihre erste Programmiersprache ist, werden Sie vielleicht ein wenig unruhig.
„WTF dieser Typ hat gerade eine beschissene Version einer Klasse neu erstellt“ – Sie
Für diejenigen, die damit nicht vertraut sind, erlaubt eine Klasse, einen Bauplan für ein Objekt zu erstellen. Wenn Sie dann eine Instanz dieser Klasse erzeugen, erhalten Sie ein Objekt mit den Eigenschaften und Methoden, die in der Blaupause definiert sind.
Kennen Sie das? Das ist im Grunde das, was wir mit unserer Animal
Konstruktorfunktion oben gemacht haben. Anstatt jedoch das class
-Schlüsselwort zu verwenden, haben wir einfach eine normale alte JavaScript-Funktion verwendet, um die gleiche Funktionalität zu erzeugen. Zugegeben, es erforderte ein wenig zusätzliche Arbeit sowie etwas Wissen darüber, was „unter der Haube“ von JavaScript passiert, aber die Ergebnisse sind die gleichen.
Hier ist die gute Nachricht. JavaScript ist keine tote Sprache. Sie wird ständig vom TC-39-Komitee verbessert und ergänzt. Das heißt, auch wenn die ursprüngliche Version von JavaScript keine Klassen unterstützte, gibt es keinen Grund, warum sie nicht in die offizielle Spezifikation aufgenommen werden können. Tatsächlich ist es genau das, was das TC-39-Komitee getan hat. Im Jahr 2015 wurde EcmaScript (die offizielle JavaScript-Spezifikation) 6 mit Unterstützung für Klassen und das Schlüsselwort class
veröffentlicht. Schauen wir uns an, wie unsere obige Animal
Konstruktorfunktion mit der neuen Klassensyntax aussehen würde.
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)
Sehr sauber, nicht wahr?
Wenn das also der neue Weg ist, Klassen zu erstellen, warum haben wir dann so viel Zeit damit verbracht, den alten Weg durchzugehen? Der Grund dafür ist, dass der neue Weg (mit dem Schlüsselwort class
) in erster Linie nur „syntaktischer Zucker“ über den bestehenden Weg ist, den wir das pseudoklassische Muster genannt haben. Um die Komfortsyntax von ES6-Klassen vollständig zu verstehen, müssen Sie zuerst das pseudoklassische Muster verstehen.
An dieser Stelle haben wir die Grundlagen des JavaScript-Prototyps behandelt. Der Rest dieses Beitrags wird sich dem Verständnis anderer „Gut zu wissen“-Themen widmen, die damit zusammenhängen. In einem anderen Beitrag werden wir uns ansehen, wie wir diese Grundlagen nutzen können, um zu verstehen, wie die Vererbung in JavaScript funktioniert.
Array-Methoden
Wir haben oben ausführlich darüber gesprochen, dass man, wenn man Methoden über Instanzen einer Klasse hinweg gemeinsam nutzen möchte, diese Methoden in den Prototyp der Klasse (oder Funktion) stecken sollte. Wir können dasselbe Muster sehen, wenn wir uns die Klasse Array
ansehen. Historisch gesehen haben Sie Ihre Arrays wahrscheinlich so erstellt
const friends =
Es stellt sich heraus, dass das nur Zucker ist, um eine new
-Instanz der Array
-Klasse zu erstellen.
const friendsWithSugar = const friendsWithoutSugar = new Array()
Eine Sache, über die Sie sich vielleicht noch nie Gedanken gemacht haben, ist, dass jede Instanz eines Arrays all diese eingebauten Methoden hat (splice
slice
pop
, etc)?
Wie Sie jetzt wissen, liegt das daran, dass diese Methoden auf Array.prototype
liegen und wenn Sie eine neue Instanz von Array
erstellen, verwenden Sie das Schlüsselwort new
, das diese Delegation an Array.prototype
bei fehlgeschlagenen Lookups einrichtet.
Wir können alle Methoden des Arrays sehen, indem wir einfach Array.prototype
protokollieren.
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()*/
Die gleiche Logik existiert auch für Objekte. Alle Objekte delegieren bei fehlgeschlagenen Suchvorgängen an Object.prototype
, weshalb alle Objekte Methoden wie toString
und hasOwnProperty
haben.
Statische Methoden
Bis zu diesem Punkt haben wir das Warum und Wie der gemeinsamen Nutzung von Methoden zwischen Instanzen einer Klasse behandelt. Was aber, wenn wir eine Methode haben, die für die Klasse wichtig ist, aber nicht von allen Instanzen geteilt werden muss? Was wäre zum Beispiel, wenn wir eine Funktion hätten, die ein Array von Animal
-Instanzen aufnimmt und bestimmt, welche als nächstes gefüttert werden muss? Wir nennen sie nextToEat
.
function nextToEat (animals) { const sortedByLeastEnergy = animals.sort((a,b) => { return a.energy - b.energy }) return sortedByLeastEnergy.name}
Es macht keinen Sinn, nextToEat
auf Animal.prototype
laufen zu lassen, da wir es nicht mit allen Instanzen teilen wollen. Stattdessen können wir es eher als eine Hilfsmethode betrachten. Wenn also nextToEat
nicht auf Animal.prototype
stehen soll, wo sollen wir es dann unterbringen? Nun, die offensichtliche Antwort ist, dass wir nextToEat
einfach in den gleichen Gültigkeitsbereich wie unsere Animal
Klasse stecken könnten und es dann referenzieren, wenn wir es brauchen, wie wir es normalerweise tun würden.
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
Nun funktioniert das, aber es gibt einen besseren Weg.
Wenn Sie eine Methode haben, die spezifisch für eine Klasse selbst ist, aber nicht für alle Instanzen dieser Klasse gemeinsam genutzt werden muss, können Sie sie als
static
Eigenschaft der Klasse hinzufügen.
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 }}
Nun, da wir nextToEat
als static
Eigenschaft der Klasse hinzugefügt haben, es lebt auf der Animal
-Klasse selbst (nicht auf ihrem Prototyp) und kann mit Animal.nextToEat
angesprochen werden.
const leo = new Animal('Leo', 7)const snoop = new Animal('Snoop', 10)console.log(Animal.nextToEat()) // Leo
Da wir in diesem Beitrag einem ähnlichen Muster gefolgt sind, lassen Sie uns einen Blick darauf werfen, wie wir das Gleiche mit ES5 erreichen würden. Im obigen Beispiel haben wir gesehen, wie mit dem Schlüsselwort static
die Methode direkt in die Klasse selbst eingefügt werden kann. Mit ES5 ist das gleiche Muster so einfach wie das manuelle Hinzufügen der Methode zum Funktionsobjekt.
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
Den Prototyp eines Objekts abrufen
Unabhängig davon, welches Muster Sie zum Erstellen eines Objekts verwendet haben, kann der Prototyp dieses Objekts mit der Methode Object.getPrototypeOf
abgerufen werden.
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
Es gibt zwei wichtige Erkenntnisse aus dem obigen Code.
Erstens werden Sie feststellen, dass proto
ein Objekt mit 4 Methoden ist, constructor
eat
sleep
, und play
. Das macht Sinn. Wir haben getPrototypeOf
verwendet, um die Instanz zu übergeben, leo
, um den Prototyp dieser Instanz zurückzubekommen, in dem alle unsere Methoden leben. Das sagt uns auch eine weitere Sache über prototype
, über die wir noch nicht gesprochen haben. Standardmäßig hat das prototype
-Objekt eine constructor
-Eigenschaft, die auf die ursprüngliche Funktion oder die Klasse verweist, aus der die Instanz erzeugt wurde. Das bedeutet auch, dass JavaScript standardmäßig eine constructor
-Eigenschaft auf den Prototyp legt, so dass jede Instanz über instance.constructor
auf ihren Konstruktor zugreifen kann.
Die zweite wichtige Erkenntnis von oben ist, dass Object.getPrototypeOf(leo) === Animal.prototype
. Auch das macht Sinn. Die Animal
Konstruktorfunktion hat eine Prototyp-Eigenschaft, mit der wir Methoden über alle Instanzen hinweg teilen können, und getPrototypeOf
erlaubt uns, den Prototyp der Instanz selbst zu sehen.
function Animal (name, energy) { this.name = name this.energy = energy}const leo = new Animal('Leo', 7)console.log(leo.constructor) // Logs the constructor function
Um an das anzuknüpfen, was wir vorhin mit Object.create
besprochen haben, funktioniert das deshalb, weil alle Instanzen von Animal
bei fehlgeschlagenen Suchvorgängen an Animal.prototype
delegieren werden. Wenn Sie also versuchen, auf leo.constructor
zuzugreifen, hat leo
keine constructor
-Eigenschaft, so dass es diese Suche an Animal.prototype
delegiert, das tatsächlich eine constructor
-Eigenschaft hat. Wenn dieser Absatz keinen Sinn ergibt, lesen Sie bitte noch einmal den Abschnitt Object.create
oben.
Sie haben vielleicht schon einmal gesehen, dass __proto__ verwendet wird, um den Prototyp einer Instanz zu erhalten. Das ist ein Relikt der Vergangenheit. Verwenden Sie stattdessen Object.getPrototypeOf(instance), wie wir oben gesehen haben.
Bestimmen, ob eine Eigenschaft im Prototyp lebt
Es gibt bestimmte Fälle, in denen Sie wissen müssen, ob eine Eigenschaft in der Instanz selbst lebt oder im Prototyp, an den das Objekt delegiert. Wir können dies in Aktion sehen, indem wir eine Schleife über unser leo
Objekt ziehen, das wir erstellt haben. Nehmen wir an, das Ziel war es, eine Schleife über leo
zu ziehen und alle seine Schlüssel und Werte zu protokollieren. Mit einer for in
-Schleife würde das wahrscheinlich so aussehen.
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}`)}
Was würden Sie erwarten zu sehen? Höchstwahrscheinlich etwas in der Art von –
Key: name. Value: LeoKey: energy. Value: 7
Was Sie jedoch sehen, wenn Sie den Code ausführen, ist dies –
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}
Warum ist das so? Nun, eine for in
-Schleife wird alle aufzählbaren Eigenschaften sowohl des Objekts selbst als auch des Prototyps, an den es delegiert ist, durchlaufen. Da standardmäßig jede Eigenschaft, die Sie zum Prototyp der Funktion hinzufügen, aufzählbar ist, sehen wir nicht nur name
und energy
, sondern wir sehen auch alle Methoden des Prototyps – eat
sleep
, und play
. Um dies zu beheben, müssen wir entweder festlegen, dass alle Prototyp-Methoden nicht aufzählbar sind, oder wir brauchen eine Möglichkeit, nur console.log zu verwenden, wenn die Eigenschaft auf dem leo
-Objekt selbst liegt und nicht auf dem Prototyp, an den leo
bei fehlgeschlagenen Lookups delegiert. Hier kann uns hasOwnProperty
helfen.
hasOwnProperty
ist eine Eigenschaft auf jedem Objekt, die ein Boolean zurückgibt, das angibt, ob das Objekt die angegebene Eigenschaft als eigene Eigenschaft hat und nicht auf dem Prototyp, an den das Objekt delegiert. Das ist genau das, was wir brauchen. Mit diesem neuen Wissen können wir nun unseren Code ändern, um die Vorteile von hasOwnProperty
innerhalb unserer for in
-Schleife zu nutzen.
...const leo = new Animal('Leo', 7)for(let key in leo) { if (leo.hasOwnProperty(key)) { console.log(`Key: ${key}. Value: ${leo}`) }}
Und jetzt sehen wir nur noch die Eigenschaften, die sich auf das leo
-Objekt selbst beziehen und nicht mehr auf den Prototyp, an den leo
delegiert.
Key: name. Value: LeoKey: energy. Value: 7
Wenn Sie immer noch ein wenig verwirrt sind über hasOwnProperty
, hier ist etwas Code, der es vielleicht aufklärt.
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
Prüfen, ob ein Objekt eine Instanz einer Klasse ist
Manchmal möchte man wissen, ob ein Objekt eine Instanz einer bestimmten Klasse ist. Dazu können Sie den instanceof
-Operator verwenden. Der Anwendungsfall ist ziemlich einfach, aber die eigentliche Syntax ist ein bisschen seltsam, wenn Sie sie noch nie gesehen haben. Es funktioniert so
object instanceof Class
Die obige Anweisung gibt true zurück, wenn object
eine Instanz von Class
ist, und false, wenn sie es nicht ist. Um zu unserem Beispiel Animal
zurückzukehren, würden wir etwa so aussehen.
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
Die Art und Weise, wie instanceof
funktioniert, ist, dass es auf das Vorhandensein von constructor.prototype
in der Prototyp-Kette des Objekts prüft. Im obigen Beispiel ist leo instanceof Animal
ein true
, weil Object.getPrototypeOf(leo) === Animal.prototype
. Außerdem ist leo instanceof User
ein false
, weil Object.getPrototypeOf(leo) !== User.prototype
.
Erstellen neuer agnostischer Konstruktorfunktionen
Können Sie den Fehler im folgenden Code erkennen?
function Animal (name, energy) { this.name = name this.energy = energy}const leo = Animal('Leo', 7)
Auch erfahrene JavaScript-Entwickler werden manchmal über das obige Beispiel stolpern. Da wir das pseudoclassical pattern
verwenden, das wir zuvor kennengelernt haben, müssen wir beim Aufruf der Animal
-Konstruktorfunktion sicherstellen, dass wir sie mit dem Schlüsselwort new
aufrufen. Wenn wir das nicht tun, wird das this
-Schlüsselwort nicht erstellt und auch nicht implizit zurückgegeben.
Zur Auffrischung: Die auskommentierten Zeilen zeigen, was hinter den Kulissen passiert, wenn Sie das new
-Schlüsselwort für eine Funktion verwenden.
function Animal (name, energy) { // const this = Object.create(Animal.prototype) this.name = name this.energy = energy // return this}
Dies scheint ein zu wichtiges Detail zu sein, um es anderen Entwicklern zu überlassen, sich daran zu erinnern. Angenommen, wir arbeiten in einem Team mit anderen Entwicklern, gibt es eine Möglichkeit, wie wir sicherstellen können, dass unser Animal
Konstruktor immer mit dem new
Schlüsselwort aufgerufen wird? Es stellt sich heraus, dass es das gibt, und zwar durch Verwendung des instanceof
-Operators, den wir zuvor kennengelernt haben.
Wenn der Konstruktor mit dem new
Schlüsselwort aufgerufen wurde, dann wird this
innerhalb des Body des Konstruktors ein instanceof
die Konstruktorfunktion selbst sein. Das waren eine Menge großer Worte. Hier ist etwas 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}
Anstatt nun einfach eine Warnung an den Konsumenten der Funktion zu protokollieren, was wäre, wenn wir die Funktion erneut aufrufen, aber diesmal mit dem new
Schlüsselwort?
function Animal (name, energy) { if (this instanceof Animal === false) { return new Animal(name, energy) } this.name = name this.energy = energy}
Jetzt ist es egal, ob Animal
mit dem new
-Schlüsselwort aufgerufen wird, es wird immer noch richtig funktionieren.
Wiedererstellen von Object.create
In diesem Beitrag haben wir uns stark auf Object.create
verlassen, um Objekte zu erstellen, die an den Prototyp der Konstruktorfunktion delegieren. An diesem Punkt sollten Sie wissen, wie Sie Object.create
in Ihrem Code verwenden können, aber eine Sache, an die Sie vielleicht noch nicht gedacht haben, ist, wie Object.create
tatsächlich unter der Haube funktioniert. Damit Sie wirklich verstehen, wie Object.create
funktioniert, werden wir es selbst nachbauen. Was wissen wir zunächst über die Funktionsweise von Object.create
?
- Es nimmt ein Argument auf, das ein Objekt ist.
- Es erzeugt ein Objekt, das bei fehlgeschlagenen Suchvorgängen an das Argument-Objekt delegiert.
- Es gibt das neu erstellte Objekt zurück.
Fangen wir mit #1 an.
Object.create = function (objToDelegateTo) {}
Einfach genug.
Nun #2 – wir müssen ein Objekt erstellen, das bei fehlgeschlagenen Suchvorgängen an das Argument-Objekt delegiert wird. Diese Aufgabe ist ein wenig kniffliger. Dazu nutzen wir unser Wissen darüber, wie das new
-Schlüsselwort und Prototypen in JavaScript funktionieren. Zuerst erstellen wir innerhalb des Body unserer Object.create
-Implementierung eine leere Funktion. Dann setzen wir den Prototyp dieser leeren Funktion gleich dem Argument object. Um ein neues Objekt zu erstellen, rufen wir dann unsere leere Funktion mit dem Schlüsselwort new
auf. Wenn wir das neu erstellte Objekt zurückgeben, ist auch die Nummer 3 fertig.
Object.create = function (objToDelegateTo) { function Fn(){} Fn.prototype = objToDelegateTo return new Fn()}
Wild. Gehen wir es durch.
Wenn wir eine neue Funktion erstellen, Fn
im obigen Code, kommt sie mit einer prototype
Eigenschaft. Wenn wir sie mit dem Schlüsselwort new
aufrufen, wissen wir, dass wir ein Objekt zurückbekommen, das bei fehlgeschlagenen Suchvorgängen an den Prototyp der Funktion delegiert wird. Wenn wir den Prototyp der Funktion überschreiben, können wir entscheiden, welches Objekt bei fehlgeschlagenen Suchvorgängen delegiert werden soll. In unserem obigen Beispiel überschreiben wir also den Prototyp von Fn
mit dem Objekt, das beim Aufruf von Object.create
übergeben wurde, das wir objToDelegateTo
nennen.
Beachten Sie, dass wir nur ein einziges Argument für Object.create unterstützen. Die offizielle Implementierung unterstützt auch ein zweites, optionales Argument, mit dem Sie dem erzeugten Objekt weitere Eigenschaften hinzufügen können.
Pfeilfunktionen
Pfeilfunktionen haben kein eigenes this
-Schlüsselwort. Daher können Pfeilfunktionen keine Konstruktorfunktionen sein, und wenn Sie versuchen, eine Pfeilfunktion mit dem Schlüsselwort new
aufzurufen, wird ein Fehler ausgegeben.
const Animal = () => {}const leo = new Animal() // Error: Animal is not a constructor
Auch weil wir oben gezeigt haben, dass das pseudoklassische Muster nicht mit Pfeilfunktionen verwendet werden kann, haben Pfeilfunktionen auch keine prototype
-Eigenschaft.
const Animal = () => {}console.log(Animal.prototype) // undefined