Każdy obiekt ma prototyp
JavaScript to język oparty na prototypach. Brzmi egzotycznie, ale idea jest prosta: każdy obiekt ma ukryte powiązanie z innym obiektem, swoim prototypem, a gdy pytasz o właściwość, której obiekt nie ma, JavaScript idzie tym powiązaniem i pyta tam.
rabbit nie ma własnej właściwości eats. JavaScript sprawdza rabbit, nie znajduje jej, idzie powiązaniem prototypu do animal, znajduje eats: true i to zwraca. W przypadku flies przechodzi cały łańcuch, nic nie znajduje i zwraca undefined.
To wyszukiwanie z przechodzeniem w górę to cały mechanizm. Dziedziczenie, metody, class: wszystko jest na nim zbudowane.
Łańcuch prototypów
Łańcuch nie kończy się na jednym kroku. Prototyp może mieć własny prototyp i tak dalej, aż dojdziesz do null:
Po uruchomieniu zobaczysz rabbit, potem Object.prototype, a potem null. Dlatego rabbit.toString() działa, chociaż nigdy nie definiowano toString: ta metoda mieszka w Object.prototype, na szczycie niemal każdego łańcucha.
Odczyt właściwości przechodzi ten łańcuch od dołu do góry. Przypisanie natomiast zawsze zapisuje do samego obiektu i nigdy nie sięga wyżej. Ta asymetria jest ważna i regularnie zaskakuje ludzi.
Funkcje konstruktora i .prototype
Zanim pojawiło się class, standardowym sposobem na tworzenie wielu podobnych obiektów była funkcja konstruktora wywoływana z new:
Gdy wywołujesz new User("Ada"), dzieją się dwie rzeczy:
- Powstaje nowy obiekt, którego prototypem jest
User.prototype. Useruruchamia się zthispowiązanym z tym nowym obiektem.
greet nie jest kopiowane do każdej instancji. Istnieje raz w User.prototype, a zarówno ada, jak i boris znajdują je, przechodząc swój łańcuch. Dlatego ostatnia linia wypisuje true: to dosłownie ta sama funkcja.
prototype a __proto__
Te dwie nazwy mylą wszystkich. Są powiązane, ale to nie to samo.
User.prototypeto właściwość funkcji konstruktora. To obiekt, który staje się prototypem instancji tworzonych przeznew User(...).ada.__proto__(alboObject.getPrototypeOf(ada)) to powiązanie w instancji, które wskazuje w górę na jej prototyp.
W nowym kodzie wybieraj Object.getPrototypeOf(obj) zamiast obj.__proto__. __proto__ to przestarzały akcesor zachowany dla kompatybilności, a funkcja jest oficjalnym API.
Klasy to lukier nad tym mechanizmem
Nowoczesny JavaScript pozwala pisać class, ale pod spodem nadal masz do czynienia z prototypami. Porównaj obie wersje:
greet trafiło do User.prototype, tak samo jak przy pisaniu ręcznym. Słowo kluczowe class daje głównie schludniejszą składnię, surowsze zasady (musisz użyć new) i czystszy sposób na extends, ale model działania jest identyczny.
Ta wiedza przydaje się przy czytaniu komunikatów o błędach i debugowaniu this. Błąd dotyczący „User.prototype.greet” to nie dziwna wewnętrzna nazwa, tylko dokładnie to miejsce, w którym mieszka metoda.
Dziedziczenie to po prostu dłuższe łańcuchy
extends łączy jeden prototyp z drugim. Prototyp rodzica staje się prototypem prototypu dziecka:
Wyszukiwanie rex.eat przechodzi rex → Dog.prototype → Animal.prototype, znajduje tam eat i wywołuje je z this nadal powiązanym z rex. To wszystko, co robi extends: ustawia za ciebie łańcuch.
Tworzenie obiektów bezpośrednio z prototypem
Konstruktor wcale nie jest potrzebny. Object.create(proto) tworzy nowy obiekt z prototypem, który wskażesz:
Bez class, bez new, bez funkcji konstruktora. Dwa obiekty współdzielą jedną metodę przez wspólny prototyp. To najbardziej surowa forma dziedziczenia prototypowego, a wszystko inne jest zbudowane na niej.
hasOwnProperty: własne a odziedziczone
Ponieważ wyszukiwanie przechodzi łańcuch, "foo" in obj zwraca true także dla właściwości odziedziczonych. Gdy chcesz odróżnić właściwość, którą obiekt naprawdę posiada, użyj Object.hasOwn (albo starszego hasOwnProperty):
name jest w instancji. greet jest w prototypie. in znajduje obie, a Object.hasOwn tylko pierwszą. Ma to znaczenie przy iterowaniu przez for...in albo serializacji obiektu: zwykle chcesz tylko właściwości własne.
Nie łataj wbudowanych prototypów
Ponieważ Array.prototype jest współdzielone przez każdą tablicę w programie, mógłbyś dodać do niego metody:
// Please don't.
Array.prototype.last = function () {
return this[this.length - 1];
};
[1, 2, 3].last(); // 3
Problem nie polega na tym, że to nie działa, bo działa. Problem polega na tym, że każda biblioteka, każda zależność i każda przyszła wersja JavaScript dzieli teraz z tobą tę przestrzeń nazw. Gdy Array.prototype.last w końcu pojawi się jako prawdziwa metoda o nieco innej semantyce, twój kod (albo cudzy) zepsuje się w subtelny sposób. Historia Array.prototype.flatten i Array.prototype.flat to klasyczna przestroga.
Trzymaj funkcje pomocnicze jako samodzielne funkcje:
O jedną współdzieloną powierzchnię kolizji mniej.
Model myślowy
Po odrzuceniu całej reszty prototypy sprowadzają się do trzech zasad:
- Każdy obiekt ma powiązanie z prototypem (może to być
null). - Odczyty właściwości idą w górę łańcucha, zapisy nie.
class,newiextendsto sposoby na zbudowanie tych łańcuchów bez samodzielnego pisaniaObject.create.
Zapamiętaj te trzy zasady, a zachowanie this, instanceof, rozwiązywanie metod i dziedziczenie ułożą się w całość.
Dalej: pętla zdarzeń
Prototypy zamykają temat modelu obiektowego. Następny rozdział dotyczy czegoś zupełnie innego: tego, jak JavaScript faktycznie wykonuje twój kod w czasie. Event loop sprawia, że timery, Promise i async/await zachowują się tak, a nie inaczej, i jest podstawą wszystkiego, co asynchroniczne.
Najczęściej zadawane pytania
Czym jest prototyp w JavaScript?
Każdy obiekt w JavaScript ma wewnętrzne powiązanie z innym obiektem, zwanym jego prototypem. Gdy odwołujesz się do właściwości, której nie ma w samym obiekcie, JavaScript idzie w górę tego powiązania, czyli łańcucha prototypów, i szuka jej tam. Dzięki temu łańcuchowi metody zdefiniowane raz są współdzielone przez wiele instancji.
Czym różni się __proto__ od prototype?
prototype to właściwość funkcji konstruktora (i klas). To obiekt, który stanie się prototypem instancji tworzonych przez new. __proto__ (albo Object.getPrototypeOf(obj)) to faktyczne powiązanie instancji z jej prototypem. Zatem instance.__proto__ === Constructor.prototype.
Czy klasy w JavaScript to tylko lukier składniowy nad prototypami?
W dużej mierze tak. class Foo { bar() {} } umieszcza bar w Foo.prototype, dokładnie tak, jakby napisać function Foo(){} i Foo.prototype.bar = function(){}. Klasy dodają pola prywatne, surowszą semantykę i wygodniejszą składnię dla extends i super, ale pod spodem nadal działają prototypy.
Czy dodawać metody do wbudowanych prototypów, takich jak Array.prototype?
Prawie nigdy. Zmiana Array.prototype albo Object.prototype wpływa na każdą tablicę lub obiekt w programie, także te z bibliotek. Może to kolidować z przyszłymi dodatkami do języka i psuć pętle for...in. Trzymaj własne funkcje pomocnicze w osobnych funkcjach lub modułach.