Menu

Prototypy w JavaScript: łańcuch za każdym obiektem

Czym naprawdę są prototypy w JavaScript, jak łańcuch prototypów rozwiązuje wyszukiwanie właściwości i jak składnia class przekłada się na ten sam mechanizm.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

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:

  1. Powstaje nowy obiekt, którego prototypem jest User.prototype.
  2. User uruchamia się z this powią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.prototype to właściwość funkcji konstruktora. To obiekt, który staje się prototypem instancji tworzonych przez new User(...).
  • ada.__proto__ (albo Object.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, new i extends to sposoby na zbudowanie tych łańcuchów bez samodzielnego pisania Object.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.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ