Jak działa prototype chain w JavaScript
W JavaScripcie obiekt może wywołać metodę, której nie ma u siebie. Metody klasy są zdefiniowane raz, na wspólnym prototype, a instancje sięgają tam linkiem. Ten mechanizm nazywa się prototype chain. Notatka o tym, jak działa i dlaczego.
W JavaScripcie obiekt może wywołać metodę, której nie ma u siebie. Klasa Circle definiuje metodę area, ale gdy zrobisz new Circle(), ta metoda nie trafia na nową instancję - jest zdefiniowana raz, na wspólnym obiekcie Circle.prototype. Każda instancja tylko wskazuje na ten obiekt linkiem - w specyfikacji nazywanym [[Prototype]].
class Circle {
constructor(r) {
this.r = r;
}
area() {
return Math.PI * this.r ** 2;
}
}
const a = new Circle(1);
a.hasOwnProperty("area"); // false
a.area(); // ale to działaarea na a nie istnieje, mimo to a.area() zwraca wynik. To dlatego, że JavaScript nie szuka właściwości tylko na samym obiekcie - chodzi też po łańcuchu prototype’ów, w górę aż znajdzie albo dojdzie do końca. Ten
mechanizm nazywa się prototype chain i jest fundamentem dziedziczenia w JS.
#Pod spodem class to obiekt
Klucz do prototype chain to jedna myśl: klasa w JavaScripcie to też obiekt - z metodami w polu prototype. Gdy piszesz class Circle { area() {} }, silnik tworzy obiekt Circle.prototype i umieszcza na nim metodę area. Każda nowa instancja Circle dostaje link do tego obiektu - swój prototype.
Ten model nazywa się prototypal inheritance.
#Lookup w łańcuchu
Mechanizm jest prosty i zawsze taki sam. Gdy silnik czyta a.area():
- Patrzy najpierw na sam obiekt
a. Nie ma tamarea. - Idzie po linku do prototype’u (
Circle.prototype). Tam jest. Wraca z wynikiem.
Jeśli na prototype’ie też jej nie ma, silnik idzie wyżej - do prototype’u prototype’u - i tak aż do końca łańcucha. Końcem jest Object.prototype, którego prototype to już null. Jeśli właściwość się po drodze nie znajdzie, dostajesz undefined.
To jest prototype chain: kolejne obiekty, przez które silnik przechodzi, gdy szuka właściwości. Sam link, po którym idzie, w runtime odczytasz przez Object.getPrototypeOf(obj) - to kanoniczny dostęp do [[Prototype]] (stary alias __proto__ jest non-standard i odradzany przez MDN).
Konsekwencja: jedna metoda w pamięci obsługuje dowolną liczbę instancji. Czy masz dziesięć instancji Circle, czy milion - area istnieje raz, na jednym Circle.prototype. Każda instancja po prostu sięga tam linkiem - nie ma metody u siebie.
#Object.create() - dziedziczenie bez new
Najczystszy sposób na pokazanie tego mechanizmu to Object.create() - metoda z ES5, która robi dokładnie dwie rzeczy: tworzy nowy pusty obiekt i ustawia mu link na obiekt, który podasz.
const animal = {
describe() {
return `Jestem ${this.name}.`;
},
};
const dog = Object.create(animal);
dog.name = "Rex";
dog.describe(); // "Jestem Rex."Tu nie ma żadnej klasy ani new. dog to po prostu obiekt z linkiem do animal. Gdy wywołujesz dog.describe(), silnik nie znajduje describe na samym dog, idzie po linku do animal, znajduje, wywołuje.
Tę samą operację robi pod spodem class extends - to jeden z elementów, które kryje za sobą:
ChildClass.prototype = Object.create(ParentClass.prototype);To jedna linijka, którą extends dorzuca za ciebie. Mówi silnikowi: prototype obiektów ChildClass ma być połączony z prototypem obiektów ParentClass. Dzięki temu silnik dociera do metod rodzica tym samym mechanizmem - idąc po linku z ChildClass.prototype do ParentClass.prototype.
#Słowo na koniec
Klasy w JavaScripcie to syntactic sugar dla dziedziczenia prototypowego - nie nowy mechanizm. Dlatego class Circle extends Shape ostatecznie sprowadza się do dwóch obiektów (Circle.prototype i Shape.prototype) połączonych linkiem, po którym silnik chodzi w górę, gdy szuka brakujących właściwości.
Ten sam mechanizm spotykasz przy konstruktorach - opisałem go krok po kroku w poście o new, gdzie widać dokładnie, co new robi z prototype’em świeżego obiektu i dlaczego klasy ES2015 to ten sam mechanizm z ładniejszym zapisem.