Przejdź do treści

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ła

area 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():

  1. Patrzy najpierw na sam obiekt a. Nie ma tam area.
  2. 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.

← Wróć do notatek