Nie można zajść daleko w JavaScript nie mając do czynienia z obiektami. Są one fundamentalne dla prawie każdego aspektu języka programowania JavaScript. W rzeczywistości, nauka tworzenia obiektów jest prawdopodobnie jedną z pierwszych rzeczy, które studiowałeś, kiedy zaczynałeś. W związku z tym, aby jak najefektywniej poznać prototypy w JavaScript, zamierzamy wykorzystać naszego wewnętrznego młodszego programistę i wrócić do podstaw.
Obiekty to pary klucz-wartość. Najczęstszym sposobem tworzenia obiektu jest użycie nawiasów klamrowych {}
a właściwości i metody dodajemy do obiektu używając notacji kropkowej.
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}
Proste. Teraz w naszej aplikacji będziemy musieli stworzyć więcej niż jedno zwierzę. Naturalnie, następnym krokiem będzie zamknięcie tej logiki wewnątrz funkcji, którą będziemy mogli wywołać za każdym razem, gdy będziemy potrzebowali utworzyć nowe zwierzę. Nazwiemy ten wzorzec Functional Instantiation
, a samą funkcję nazwiemy „funkcją konstruktora”, ponieważ odpowiada ona za „skonstruowanie” nowego obiektu.
Funkcja Instancja
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
Tak jest. Dojdziemy do tego.
Teraz, gdy chcemy stworzyć nowe zwierzę (lub szerzej mówiąc nową „instancję”), wszystko, co musimy zrobić, to wywołać naszą Animal
funkcję, przekazując jej zwierzę name
i energy
poziom. Działa to świetnie i jest niesamowicie proste. Jednak, czy potrafisz dostrzec jakieś słabe punkty tego wzorca? Największa z nich i ta, którą postaramy się rozwiązać, dotyczy trzech metod – eat
sleep
oraz play
. Każda z tych metod jest nie tylko dynamiczna, ale również całkowicie generyczna. Oznacza to, że nie ma powodu, aby ponownie tworzyć te metody, tak jak robimy to obecnie, gdy tworzymy nowe zwierzę. Marnujemy tylko pamięć i sprawiamy, że każdy obiekt zwierzęcia jest większy niż powinien. Czy możesz wymyślić jakieś rozwiązanie? Co by było, gdyby zamiast tworzyć te metody od nowa za każdym razem, gdy tworzymy nowe zwierzę, przenieść je do ich własnego obiektu, a następnie każde zwierzę mogło odwoływać się do tego obiektu? Możemy nazwać ten wzorzec Functional Instantiation with Shared Methods
, słowny, ale opisowy.
Funkcjonalna instancja z metodami współdzielonymi
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)
Przenosząc metody współdzielone do ich własnego obiektu i odwołując się do tego obiektu wewnątrz naszej Animal
funkcji, rozwiązaliśmy teraz problem marnowania pamięci i zbyt dużych obiektów zwierząt.
Object.create
Poprawmy nasz przykład jeszcze raz, używając Object.create
. Mówiąc najprościej, Object.create pozwala na stworzenie obiektu, który będzie delegował do innego obiektu w przypadku nieudanego wyszukiwania. Mówiąc inaczej, Object.create pozwala na stworzenie obiektu, który za każdym razem, gdy nie powiedzie się wyszukiwanie właściwości na tym obiekcie, może skonsultować się z innym obiektem, aby sprawdzić, czy ten inny obiekt posiada daną właściwość. To było dużo słów. Zobaczmy trochę kodu.
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
Więc w powyższym przykładzie, ponieważ child
został utworzony z Object.create(parent)
, za każdym razem, gdy wystąpi nieudane wyszukiwanie właściwości na child
, JavaScript przekaże to wyszukiwanie do obiektu parent
. Oznacza to, że nawet jeśli child
nie ma właściwości heritage
parent
ma, więc kiedy zalogujesz się child.heritage
otrzymasz dziedzictwo parent
, które było Irish
.
Mając już Object.create
w naszej szopie, jak możemy go użyć, aby uprościć nasz Animal
kod z wcześniejszego kodu? Cóż, zamiast dodawać wszystkie współdzielone metody do zwierzęcia jedna po drugiej, tak jak robimy to teraz, możemy użyć Object.create do delegowania do obiektu animalMethods
. Aby brzmieć naprawdę mądrze, nazwijmy to Functional Instantiation with Shared Methods and Object.create
🙃
Funkcjonalna instancja z metodami współdzielonymi i 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)
📈 Tak więc teraz, gdy wywołamy leo.eat
, JavaScript będzie szukał metody eat
na obiekcie leo
. To wyszukiwanie nie powiedzie się, a następnie, z powodu Object.create, deleguje się do obiektu animalMethods
, w którym znajdzie eat
.
Jak na razie wszystko jest w porządku. Wciąż jednak jest kilka ulepszeń, które możemy wprowadzić. Wydaje się to trochę „hacky”, że trzeba zarządzać oddzielnym obiektem (animalMethods
), aby współdzielić metody między instancjami. To wydaje się być wspólną cechą, którą chciałbyś zaimplementować w samym języku. Okazuje się, że tak i jest to powód, dla którego tu jesteś – prototype
.
Więc czym dokładnie jest prototype
w JavaScript? Cóż, najprościej mówiąc, każda funkcja w JavaScript ma właściwość prototype
, która odwołuje się do obiektu. Antyklimatyczne, prawda? Sprawdź to sam.
function doThing () {}console.log(doThing.prototype) // {}
A co jeśli zamiast tworzyć osobny obiekt do zarządzania naszymi metodami (jak to robimy z animalMethods
), po prostu umieścimy każdą z tych metod na prototypie funkcji Animal
? Wtedy wszystko, co musielibyśmy zrobić, to zamiast używać Object.create do delegowania do animalMethods
, moglibyśmy użyć go do delegowania do Animal.prototype
. Nazwiemy ten wzorzec Prototypal Instantiation
.
Prototypowa instancjacja
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)
👏👏👏 Mam nadzieję, że właśnie miałeś duży moment „aha”. Ponownie, prototype
jest po prostu właściwością, którą posiada każda funkcja w JavaScript i jak widzieliśmy powyżej, pozwala nam dzielić metody pomiędzy wszystkie instancje funkcji. Cała nasza funkcjonalność jest wciąż taka sama, ale teraz zamiast zarządzać oddzielnym obiektem dla wszystkich metod, możemy po prostu użyć innego obiektu, który jest wbudowany w samą funkcję Animal
Animal.prototype
.
Let’s. Go. Deeper.
W tym momencie wiemy już trzy rzeczy:
- Jak stworzyć funkcję konstruktora.
- Jak dodać metody do prototypu funkcji konstruktora.
- Jak użyć Object.create do delegowania nieudanych poszukiwań do prototypu funkcji.
Te trzy zadania wydają się być fundamentalne dla każdego języka programowania. Czy JavaScript jest naprawdę tak zły, że nie ma łatwiejszego, „wbudowanego” sposobu na osiągnięcie tego samego? Jak zapewne domyślasz się w tym momencie, istnieje, i to przy użyciu słowa kluczowego new
.
Co jest miłe w powolnym, metodycznym podejściu, które przyjęliśmy, aby dostać się tutaj, to teraz będziesz miał głębokie zrozumienie dokładnie tego, co słowo kluczowe new
w JavaScript robi pod maską.
Patrząc wstecz na nasz Animal
konstruktor, dwie najważniejsze części to tworzenie obiektu i zwracanie go. Bez utworzenia obiektu za pomocą Object.create
, nie moglibyśmy delegować do prototypu funkcji nieudanych poszukiwań. Bez deklaracji return
, nigdy nie odzyskalibyśmy utworzonego obiektu.
function Animal (name, energy) { let animal = Object.create(Animal.prototype) animal.name = name animal.energy = energy return animal}
Tutaj jest fajna rzecz w new
– kiedy wywołujesz funkcję używając słowa kluczowego new
, te dwie linie są wykonywane dla ciebie niejawnie („pod maską”), a obiekt, który jest tworzony, nazywa się this
.
Używając komentarzy, aby pokazać, co dzieje się pod maską i zakładając, że konstruktor Animal
jest wywoływany za pomocą słowa kluczowego new
, można to przepisać jako to.
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)
i bez komentarzy „pod maską”
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)
Znowu powód, dla którego to działa i że
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)
. działa i że obiekt this
został dla nas utworzony jest to, że wywołaliśmy funkcję konstruktora za pomocą słowa kluczowego new
. Jeśli opuścisz new
podczas wywoływania funkcji, obiekt this
nigdy nie zostanie utworzony, ani nie zostanie niejawnie zwrócony. Problem z tym możemy zobaczyć na poniższym przykładzie.
function Animal (name, energy) { this.name = name this.energy = energy}const leo = Animal('Leo', 7)console.log(leo) // undefined
Nazwa tego wzorca to Pseudoclassical Instantiation
.
Jeśli JavaScript nie jest twoim pierwszym językiem programowania, możesz być już trochę niespokojny.
„WTF ten koleś właśnie stworzył bardziej gównianą wersję klasy” – Ty
Dla tych, którzy nie są zaznajomieni, klasa pozwala na stworzenie wzoru dla obiektu. Następnie za każdym razem, gdy tworzysz instancję tej klasy, otrzymujesz obiekt z właściwościami i metodami zdefiniowanymi w blueprincie.
Sound familiar? To jest w zasadzie to, co zrobiliśmy z naszym Animal
konstruktorem funkcji powyżej. Jednak zamiast używać słowa kluczowego class
, po prostu użyliśmy zwykłej starej funkcji JavaScript, aby odtworzyć tę samą funkcjonalność. Przyznaję, wymagało to trochę dodatkowej pracy, jak również pewnej wiedzy na temat tego, co dzieje się „pod maską” JavaScriptu, ale wyniki są takie same.
Tutaj jest dobra wiadomość. JavaScript nie jest martwym językiem. Jest ciągle ulepszany i uzupełniany przez komitet TC-39. Oznacza to, że nawet jeśli początkowa wersja JavaScriptu nie wspierała klas, nie ma powodu, dla którego nie mogłyby one zostać dodane do oficjalnej specyfikacji. W rzeczywistości, to jest dokładnie to, co zrobił komitet TC-39. W 2015 roku EcmaScript (oficjalna specyfikacja JavaScript) 6 została wydana ze wsparciem dla klas i słowa kluczowego class
. Zobaczmy jak nasza Animal
funkcja konstruktora powyżej wyglądałaby z nową składnią klas.
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?
So jeśli to jest nowy sposób tworzenia klas, dlaczego spędziliśmy tyle czasu przechodząc nad starym sposobem? Powodem tego jest fakt, że nowy sposób (ze słowem kluczowym class
) jest przede wszystkim tylko „cukrem składniowym” w stosunku do istniejącego sposobu, który nazwaliśmy pseudo-klasycznym wzorcem. Aby w pełni zrozumieć wygodną składnię klas ES6, musisz najpierw zrozumieć pseudo-klasyczny wzorzec.
W tym momencie omówiliśmy podstawy prototypu JavaScript. Pozostała część tego postu będzie poświęcona zrozumieniu innych „dobrych do poznania” tematów z nim związanych. W innym poście, przyjrzymy się jak możemy wykorzystać te podstawy i użyć ich do zrozumienia jak działa dziedziczenie w JavaScript.
Metody tablicowe
Przed chwilą mówiliśmy szczegółowo o tym, że jeśli chcesz dzielić metody pomiędzy instancjami klasy, powinieneś umieścić te metody w prototypie klasy (lub funkcji). Możemy zobaczyć ten sam wzorzec, jeśli spojrzymy na klasę Array
. Historycznie prawdopodobnie tworzyłeś swoje tablice w ten sposób
const friends =
Okazuje się, że to tylko cukier nad tworzeniem new
instancji klasy Array
.
const friendsWithSugar = const friendsWithoutSugar = new Array()
Jedną z rzeczy, o których być może nigdy nie myślałeś, jest to, w jaki sposób każda instancja tablicy ma wszystkie te wbudowane metody (splice
slice
pop
, itp)?
Cóż, jak teraz wiesz, to dlatego, że te metody żyją na Array.prototype
i kiedy tworzysz nową instancję Array
, używasz słowa kluczowego new
, które ustawia delegację do Array.prototype
na nieudanych lookach.
Możemy zobaczyć wszystkie metody tablicy po prostu logując się 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()*/
Dokładnie ta sama logika istnieje również dla Obiektów. Wszystkie obiekty będą delegować do Object.prototype
nieudane wyszukiwanie, dlatego wszystkie obiekty mają metody takie jak toString
i hasOwnProperty
.
Metody statyczne
Do tego momentu omówiliśmy dlaczego i jak dzielić metody pomiędzy instancjami klasy. Jednakże, co by się stało, gdybyśmy mieli metodę, która jest ważna dla klasy, ale nie musi być współdzielona pomiędzy instancjami? Na przykład, co by było, gdybyśmy mieli funkcję, która pobierałaby tablicę Animal
instancji i określała, która z nich powinna być podana jako następna? Nazwiemy ją nextToEat
.
function nextToEat (animals) { const sortedByLeastEnergy = animals.sort((a,b) => { return a.energy - b.energy }) return sortedByLeastEnergy.name}
Nie ma sensu, aby nextToEat
żył na Animal.prototype
, ponieważ nie chcemy go dzielić między wszystkie instancje. Zamiast tego możemy myśleć o tym jako o bardziej pomocniczej metodzie. Więc jeśli nextToEat
nie powinien mieszkać na Animal.prototype
, gdzie powinniśmy go umieścić? Cóż, oczywistą odpowiedzią jest to, że moglibyśmy po prostu przykleić nextToEat
w tym samym zakresie co nasza klasa Animal
a następnie odwołać się do niej, gdy jej potrzebujemy, tak jak normalnie byśmy to robili.
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
Teraz to działa, ale jest lepszy sposób.
Gdy masz metodę, która jest specyficzna dla klasy, ale nie musi być współdzielona przez wszystkie instancje tej klasy, możesz dodać ją jako
static
właściwość klasy.
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 }}
Teraz, ponieważ dodaliśmy nextToEat
jako static
właściwość na klasie, mieszka ona w samej klasie Animal
(nie w jej prototypie) i można się do niej dostać za pomocą Animal.nextToEat
.
const leo = new Animal('Leo', 7)const snoop = new Animal('Snoop', 10)console.log(Animal.nextToEat()) // Leo
Ponieważ podążaliśmy za podobnym wzorcem w całym tym poście, spójrzmy jak osiągnęlibyśmy to samo używając ES5. W powyższym przykładzie widzieliśmy jak użycie słowa kluczowego static
spowoduje umieszczenie metody bezpośrednio w samej klasie. W ES5, ten sam wzorzec jest tak prosty, jak ręczne dodanie metody do obiektu funkcji.
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
Uzyskanie prototypu obiektu
Niezależnie od tego, którego wzorca użyłeś do stworzenia obiektu, uzyskanie prototypu tego obiektu może być osiągnięte przy użyciu metody 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
Z powyższego kodu można wyciągnąć dwa ważne wnioski.
Po pierwsze, zauważysz, że jest obiektem z 4 metodami, constructor
eat
sleep
, i play
. To ma sens. Użyliśmy getPrototypeOf
przekazując instancję, leo
otrzymując z powrotem prototyp tej instancji, czyli miejsce, w którym żyją wszystkie nasze metody. To mówi nam jeszcze jedną rzecz o prototype
, o której jeszcze nie mówiliśmy. Domyślnie, obiekt prototype
będzie miał właściwość constructor
, która wskazuje na oryginalną funkcję lub klasę, z której została utworzona instancja. Oznacza to również, że ponieważ JavaScript domyślnie umieszcza właściwość constructor
na prototypie, każda instancja będzie mogła uzyskać dostęp do swojego konstruktora poprzez instance.constructor
.
Drugim ważnym wnioskiem z powyższego jest to, że Object.getPrototypeOf(leo) === Animal.prototype
. To również ma sens. Funkcja konstruktora Animal
ma właściwość prototypu, w której możemy dzielić metody pomiędzy wszystkie instancje, a getPrototypeOf
pozwala nam zobaczyć prototyp samej instancji.
function Animal (name, energy) { this.name = name this.energy = energy}const leo = new Animal('Leo', 7)console.log(leo.constructor) // Logs the constructor function
Aby powiązać to, o czym rozmawialiśmy wcześniej z Object.create
, powodem, dla którego to działa, jest to, że wszelkie instancje Animal
będą delegować do Animal.prototype
przy nieudanych lookups. Więc kiedy próbujesz uzyskać dostęp do leo.constructor
leo
nie ma właściwości constructor
, więc będzie delegować to lookup do Animal.prototype
, który rzeczywiście ma właściwość constructor
. Jeśli ten akapit nie miał sensu, wróć i przeczytaj o Object.create
powyżej.
Mogłeś zobaczyć __proto__ używane wcześniej, aby uzyskać prototyp instancji. To już relikt przeszłości. Zamiast tego, użyj Object.getPrototypeOf(instance) jak widzieliśmy powyżej.
Determinowanie czy właściwość żyje na prototypie
Są pewne przypadki, w których musisz wiedzieć czy właściwość żyje na samej instancji czy na prototypie, do którego obiekt deleguje. Możemy to zobaczyć w akcji, przechodząc przez nasz obiekt leo
, który tworzyliśmy. Załóżmy, że celem jest zapętlenie leo
i zapisanie wszystkich jego kluczy i wartości. Używając pętli for in
, wyglądałoby to prawdopodobnie tak.
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}`)}
Co spodziewałbyś się zobaczyć? Najprawdopodobniej byłoby to coś takiego –
Key: name. Value: LeoKey: energy. Value: 7
Jednakże to, co zobaczyłeś po uruchomieniu kodu, to było to –
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}
Dlaczego tak jest? Cóż, pętla for in
będzie zapętlać wszystkie wyliczalne właściwości zarówno samego obiektu, jak i prototypu, do którego jest delegowany. Ponieważ domyślnie każda właściwość dodana do prototypu funkcji jest wyliczalna, widzimy nie tylko name
i energy
, ale również widzimy wszystkie metody na prototypie – eat
sleep
, oraz play
. Aby to naprawić, musimy albo określić, że wszystkie metody prototypu są nieenumerowalne, albo potrzebujemy sposobu, aby tylko console.log, jeśli właściwość jest na samym obiekcie leo
, a nie prototypie, który leo
deleguje do nieudanego wyszukiwania. To właśnie tutaj hasOwnProperty
może nam pomóc.
hasOwnProperty
jest właściwością na każdym obiekcie, która zwraca boolean wskazujący, czy obiekt ma określoną właściwość jako własną właściwość, a nie na prototypie, do którego obiekt deleguje. To jest dokładnie to, czego potrzebujemy. Teraz z tą nową wiedzą, możemy zmodyfikować nasz kod, aby wykorzystać hasOwnProperty
wewnątrz naszej for in
pętli.
...const leo = new Animal('Leo', 7)for(let key in leo) { if (leo.hasOwnProperty(key)) { console.log(`Key: ${key}. Value: ${leo}`) }}
I teraz to, co widzimy, to tylko właściwości, które są na samym obiekcie leo
, a nie na prototypie leo
, do którego również deleguje.
Key: name. Value: LeoKey: energy. Value: 7
Jeśli nadal jesteś nieco zdezorientowany co do hasOwnProperty
, tutaj jest trochę kodu, który może to wyjaśnić.
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
Sprawdź, czy obiekt jest instancją klasy
Czasami chcemy wiedzieć, czy obiekt jest instancją konkretnej klasy. Aby to zrobić, możesz użyć operatora instanceof
. Przypadek użycia jest dość prosty, ale faktyczna składnia jest trochę dziwna, jeśli nigdy wcześniej jej nie widziałeś. Działa to tak
object instanceof Class
Powyższe stwierdzenie zwróci true jeśli object
jest instancją Class
i false jeśli nie jest. Wracając do naszego przykładu Animal
mielibyśmy coś takiego.
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
Sposób w jaki działa instanceof
polega na tym, że sprawdza obecność constructor.prototype
w łańcuchu prototypów obiektu. W powyższym przykładzie leo instanceof Animal
jest true
, ponieważ Object.getPrototypeOf(leo) === Animal.prototype
. Ponadto, leo instanceof User
jest false
ponieważ Object.getPrototypeOf(leo) !== User.prototype
.
Tworzenie nowych agnostycznych funkcji konstruktora
Czy potrafisz dostrzec błąd w poniższym kodzie?
function Animal (name, energy) { this.name = name this.energy = energy}const leo = Animal('Leo', 7)
Nawet wytrawni programiści JavaScriptu będą czasami potykać się na powyższym przykładzie. Ponieważ używamy pseudoclassical pattern
, o którym dowiedzieliśmy się wcześniej, kiedy wywoływana jest funkcja konstruktora Animal
, musimy się upewnić, że wywołujemy ją za pomocą słowa kluczowego new
. Jeśli tego nie zrobimy, wtedy słowo kluczowe this
nie zostanie utworzone i nie zostanie również niejawnie zwrócone.
Przypominamy, że skomentowane linie są tym, co dzieje się za kulisami, gdy używasz słowa kluczowego new
na funkcji.
function Animal (name, energy) { // const this = Object.create(Animal.prototype) this.name = name this.energy = energy // return this}
To wydaje się być zbyt ważnym szczegółem, aby pozostawiać go do zapamiętania innym programistom. Zakładając, że pracujemy w zespole z innymi programistami, czy istnieje sposób, w jaki moglibyśmy zapewnić, że nasz Animal
konstruktor jest zawsze wywoływany za pomocą słowa kluczowego new
? Okazuje się, że tak i to za pomocą operatora instanceof
, o którym uczyliśmy się wcześniej.
Jeśli konstruktor został wywołany za pomocą słowa kluczowego new
, to this
wewnątrz ciała konstruktora będzie instanceof
samą funkcją konstruktora. To było dużo wielkich słów. Oto trochę kodu.
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}
Teraz zamiast po prostu logować ostrzeżenie do konsumenta funkcji, co jeśli ponownie wywołamy funkcję, ale tym razem ze słowem kluczowym new
?
function Animal (name, energy) { if (this instanceof Animal === false) { return new Animal(name, energy) } this.name = name this.energy = energy}
Teraz niezależnie od tego, czy Animal
zostanie wywołany ze słowem kluczowym new
, nadal będzie działał poprawnie.
Powtórzenie Object.create
Przez cały ten post, polegaliśmy w dużej mierze na Object.create
w celu tworzenia obiektów, które delegują do prototypu funkcji konstruktora. W tym momencie powinieneś już wiedzieć, jak używać Object.create
w swoim kodzie, ale jedną z rzeczy, o której być może nie pomyślałeś, jest to, jak Object.create
działa pod maską. Abyś mógł naprawdę zrozumieć, jak działa Object.create
, sami go stworzymy. Po pierwsze, co wiemy o tym, jak działa Object.create
?
- Przyjmuje argument, który jest obiektem.
- Tworzy obiekt, który deleguje do obiektu argumentu przy nieudanych wyszukiwaniach.
- Zwraca nowo utworzony obiekt.
Zacznijmy od #1.
Object.create = function (objToDelegateTo) {}
Dosyć proste.
Teraz #2 – musimy stworzyć obiekt, który będzie delegował do obiektu argumentu przy nieudanych wyszukiwaniach. To już jest trochę bardziej skomplikowane. Aby to zrobić, użyjemy naszej wiedzy o tym, jak słowo kluczowe new
i prototypy działają w JavaScript. Najpierw, wewnątrz ciała naszej implementacji Object.create
, utworzymy pustą funkcję. Następnie, ustawimy prototyp tej pustej funkcji na obiekt argumentu. Następnie, aby utworzyć nowy obiekt, wywołamy naszą pustą funkcję za pomocą słowa kluczowego new
. Jeśli zwrócimy ten nowo utworzony obiekt, to również zakończy to #3.
Object.create = function (objToDelegateTo) { function Fn(){} Fn.prototype = objToDelegateTo return new Fn()}
Dziwne. Przejdźmy przez to.
Gdy tworzymy nową funkcję, Fn
w kodzie powyżej, przychodzi ona z właściwością prototype
. Kiedy wywołujemy ją za pomocą słowa kluczowego new
, wiemy, że otrzymamy obiekt, który będzie delegował do prototypu funkcji przy nieudanych próbach wyszukiwania. Jeśli nadpiszemy prototyp funkcji, wtedy możemy zdecydować, do którego obiektu delegować przy nieudanych wyszukiwaniach. Tak więc w naszym przykładzie powyżej, nadpisujemy prototyp Fn
z obiektem, który został przekazany, gdy Object.create
został wywołany, który nazywamy objToDelegateTo
.
Zauważ, że obsługujemy tylko jeden argument do Object.create. Oficjalna implementacja obsługuje również drugi, opcjonalny argument, który pozwala dodać więcej właściwości do utworzonego obiektu.
Funkcje strzałkowe
Funkcje strzałkowe nie posiadają własnego this
słowa kluczowego. W rezultacie, funkcje strzałek nie mogą być funkcjami konstruktora i jeśli spróbujesz wywołać funkcję strzałki za pomocą słowa kluczowego new
, wyrzuci ona błąd.
const Animal = () => {}const leo = new Animal() // Error: Animal is not a constructor
Ponieważ wykazaliśmy powyżej, że pseudoklasyczny wzorzec nie może być używany z funkcjami strzałek, funkcje strzałek nie mają również właściwości prototype
.
const Animal = () => {}console.log(Animal.prototype) // undefined