Kobieta siedzi w biurze

Czym różnią się prototype i __proto__ w JavaScript?

9 min. czytania

prototype i __proto__ w JavaScript odnoszą się do tego samego mechanizmu dziedziczenia prototypowego, ale działają na różnych poziomach: prototype należy do funkcji (konstruktorów) i opisuje „wzorzec” dla przyszłych obiektów, a __proto__ należy do konkretnych obiektów i wskazuje na ich prototyp (faktyczny element łańcucha dziedziczenia).

Poniżej znajdziesz praktyczne wyjaśnienie – od „klasycznych” konstruktorów po współczesną składnię class – z myślą o twórcach aplikacji webowych.

1. Dlaczego w ogóle prototypy są ważne?

JavaScript opiera się na dziedziczeniu prototypowym – nawet składnia class jest tylko cukrem składniowym nad prototypami.

W skrócie, w modelu wyszukiwania właściwości silnik JS najpierw sprawdza obiekt, a jeśli nie znajdzie pasującej nazwy, przechodzi po łańcuchu prototypów aż do null. To właśnie ten łańcuch decyduje, skąd „biorą się” metody i właściwości niewystępujące bezpośrednio na obiekcie.

prototype i __proto__ to dwa różne interfejsy pracy z tym samym mechanizmem.

2. Wewnętrzna właściwość [[Prototype]] – punkt wyjścia

Kluczowe fakty o [[Prototype]]:

  • to wewnętrzna, ukryta właściwość obiektu opisana w specyfikacji,
  • nie można jej odczytać ani ustawić bezpośrednio nazwą [[Prototype]] w kodzie,
  • jest to „link” do obiektu, z którego obecny obiekt dziedziczy właściwości i metody.

W praktyce dostęp do [[Prototype]] uzyskujemy za pomocą akcesorów historycznych lub nowoczesnego API. Współcześnie stosuj Object.getPrototypeOf(obj), Object.setPrototypeOf(obj, proto) oraz Object.create(proto) zamiast bezpośredniego użycia __proto__.

prototype służy do zdefiniowania tego, co później stanie się wartością [[Prototype]] nowo utworzonych obiektów.

3. prototype – własność funkcji (konstruktorów)

Do kogo należy prototype?

prototype to własność funkcji (zwłaszcza konstruktorów wywoływanych przez new), której używamy, gdy traktujemy funkcję jako „projekt” przyszłych instancji.

Przykład:

function Person(name) { this.name = name; }

Person.prototype.sayHello = function () {
return `Cześć, jestem ${this.name}`;
};

W tym układzie Person.prototype to zwykły obiekt, do którego dopisujemy metody i właściwości, a wszystkie instancje utworzone przez new Person(...) będą z niego dziedziczyć.

Co dzieje się przy new?

Wywołanie konstruktora inicjuje typowy, czterostopniowy proces:

  1. Tworzony jest nowy obiekt: {}.
  2. Jego wewnętrzna właściwość [[Prototype]] jest ustawiana na Person.prototype.
  3. Funkcja Person jest wywoływana z this wskazującym na ten nowy obiekt.
  4. Jeśli konstruktor nie zwróci innego obiektu, wynik to ten nowy obiekt.

Efekt możesz sprawdzić w konsoli:

const jan = new Person('Jan');
jan.__proto__ === Person.prototype; // true

prototype funkcji konstruktora staje się prototypem ([[Prototype]]) tworzonych przez nią instancji.

Do czego używamy prototype?

Najczęstsze zastosowania to:

  • dodawanie metod współdzielonych przez wszystkie instancje (np. sayHello, toString),
  • realizacja dziedziczenia (łączenie prototypów różnych konstruktorów),
  • unikanie duplikowania tych samych metod w każdej instancji.

Przykład dodawania kolejnej metody:

Person.prototype.getInitials = function () {
return this.name
.split(' ')
.map((s) => s + '.')
.join('');
};

Wszystkie istniejące i przyszłe instancje Person natychmiast „widzą” tę metodę dzięki łańcuchowi prototypów.

4. __proto__ – własność obiektów

Czym jest __proto__?

__proto__ to historyczny akcesor (getter/setter) logicznie dostępny na obiektach, który zapewnia dostęp do ich [[Prototype]]. Pozwala sprawdzić, z jakiego prototypu obiekt dziedziczy, a nawet ten prototyp zmienić (co nie jest zalecane w nowym kodzie).

Porównanie dwóch sposobów odczytu prototypu instancji:

const jan = new Person('Jan');

console.log(jan.__proto__ === Person.prototype); // true
console.log(Object.getPrototypeOf(jan) === Person.prototype); // true

Obie linie zwracają true, ale druga używa nowszego, zalecanego sposobu.

Z punktu widzenia specyfikacji każdy obiekt ma wewnętrzne [[Prototype]], a __proto__ (tam, gdzie istnieje) po prostu pozwala się do niego dostać.

Przykład ręcznego ustawienia prototypu (dawny sposób):

const animal = { type: 'animal' };
const dog = { name: 'Reksio' };

dog.__proto__ = animal; // ustawiamy prototyp dog na animal
console.log(dog.type); // "animal" – szukane w prototypie

Nowoczesny odpowiednik:

const animal = { type: 'animal' };
const dog = Object.create(animal); // [[Prototype]] = animal
dog.name = 'Reksio';

console.log(dog.type); // "animal"

5. Najważniejsze różnice: prototype vs __proto__

Poniższa tabela syntetyzuje różnice w praktycznym użyciu:

Cecha prototype __proto__
Należy do funkcji (konstruktorów) obiektów (instancji)
Rola definiuje prototyp przyszłych instancji wskazuje aktualny prototyp danego obiektu
Moment użycia przy projektowaniu „klasy” / konstruktora przy analizie lub manipulacji konkretnej instancji
Typ zwykły obiekt akcesor do wewnętrznego [[Prototype]]
Jak działa z new new ustawia [[Prototype]] na fn.prototype instancja po new ma obj.__proto__ === fn.prototype
Nowoczesna alternatywa składnia class { ... } Object.getPrototypeOf, Object.setPrototypeOf
Status normalna, oficjalna część języka historyczna (legacy), mniej zalecana w nowym kodzie

6. __proto__ jest przestarzałe (legacy) – i dlaczego to ważne

__proto__ nie jest gwarantowane na wszystkich obiektach i implementacjach, dlatego jego użycie bywa nieprzewidywalne i niespójne.

Zamiast __proto__ stosuj standardowe metody pracy z prototypem:

  • Object.getPrototypeOf(obj) – odczyt wewnętrznego [[Prototype]] obiektu;
  • Object.setPrototypeOf(obj, proto) – ustawienie [[Prototype]] istniejącego obiektu;
  • Object.create(proto) – tworzenie nowego obiektu z zadanym prototypem.

W nowym kodzie preferuj API Object.*, a __proto__ traktuj głównie jako kontekst do zrozumienia starszych materiałów lub logów narzędzi developerskich.

7. Praktyczne przykłady dla webdevelopera

„Klasyczny” konstruktor + prototype

Załóżmy, że budujesz prostą aplikację i chcesz mieć obiekt Button z metodami współdzielonymi między instancjami:

function Button(label) {
this.label = label;
}

Button.prototype.render = function () {
const btn = document.createElement('button');
btn.textContent = this.label;
return btn;
};

Button.prototype.disable = function (element) {
element.setAttribute('disabled', 'true');
};

Użycie:

const saveBtn = new Button('Zapisz');
const saveElement = saveBtn.render();
document.body.appendChild(saveElement);

// Metoda disable pochodzi z prototypu
saveBtn.disable(saveElement);

Tutaj warto zapamiętać trzy rzeczy:

  • Button.prototype przechowuje metody render i disable,
  • każda instancja Button (saveBtn, cancelBtn itd.) dziedziczy je przez swój [[Prototype]],
  • instancja nie ma właściwości prototype, ale ma __proto__ wskazujące na Button.prototype.

Sprawdzenie prototypu instancji

Poniższe wywołania pokazują różnicę między prototype a __proto__ na instancji:

console.log(saveBtn.prototype); // undefined – instancja nie ma "prototype"
console.log(saveBtn.__proto__); // obiekt prototypu (Button.prototype)
console.log(Object.getPrototypeOf(saveBtn) === Button.prototype); // true

prototype działa na funkcjach/konstruktorach, a nie na instancjach.

Tworzenie obiektu z określonym prototypem – API Object.create

Zamiast bezpośredniego ustawiania __proto__ lepiej skorzystać z fabrycznej funkcji Object.create:

const base = { role: 'admin' };
const user = Object.create(base); // [[Prototype]] = base
user.name = 'Anna';
console.log(user.role); // "admin"

Zyskujesz:

  • czytelność,
  • zgodność ze standardem,
  • przewidywalne zachowanie w różnych środowiskach.

8. Łańcuch prototypów w praktyce

Poniższy przykład pokazuje dziedziczenie między konstruktorami:

function Animal() {}
Animal.prototype.eat = function () {
return 'jem';
};

function Dog(name) {
this.name = name;
}

Dog.prototype = Object.create(Animal.prototype); // dziedziczenie
Dog.prototype.bark = function () {
return 'hau';
};

const reksio = new Dog('Reksio');

Łańcuch prototypów przyjmuje postać:

reksio --> Dog.prototype --> Animal.prototype --> Object.prototype --> null

Wynik wyszukiwania metod wygląda następująco:

reksio.bark(); // znalezione na Dog.prototype
reksio.eat(); // nie ma na reksio ani na Dog.prototype -> szukamy dalej -> Animal.prototype
reksio.toString(); // brak na reksio/Dog/Animal -> Object.prototype

prototype konstruktorów tworzy strukturę klasopodobną, a Object.getPrototypeOf pozwala prześledzić tę strukturę od poziomu instancji.

9. Częste pułapki związane z prototype i __proto__

Nadpisywanie vs modyfikowanie prototype

Różnica między pełnym nadpisaniem obiektu prototype a dopisywaniem do istniejącego obiektu jest istotna:

// Nadpisanie całego obiektu prototype
Dog.prototype = {
bark() {
return 'hau';
}
};

kontra:

// Dodanie metody do istniejącego prototype
Dog.prototype.bark = function () {
return 'hau';
};

W pierwszym przypadku tracisz m.in. domyślne pole constructor wskazujące na Dog, a instancje utworzone przed zmianą zachowają stary prototyp, podczas gdy nowe otrzymają nowy.

Dlatego często stosuje się:

Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // ręczne przywrócenie konstruktora

Mylenie prototype i __proto__ na instancjach

Oto częsty błąd i właściwe sprawdzenie:

const dog = new Dog('Reksio');

dog.prototype; // undefined – to NIE jest konstruktor
dog.__proto__; // obiekt będący prototypem, równa się Dog.prototype

Zasada jest prosta: jeśli chcesz zobaczyć, co dziedziczy instancja, użyj Object.getPrototypeOf; jeśli chcesz zdefiniować zachowanie instancji, modyfikuj Constructor.prototype.

Używanie __proto__ w nowym kodzie

Manipulowanie prototypem istniejących obiektów w trakcie działania bywa kosztowne wydajnościowo i nie wszystkie obiekty muszą mieć __proto__.

Zalecenie:

  • do odczytu prototypu: Object.getPrototypeOf(obj),
  • do ustawiania prototypu nowego obiektu: Object.create(proto),
  • zmianę prototypu istniejącego obiektu (Object.setPrototypeOf) traktować raczej jako operację wyjątkową.

10. prototype i __proto__ a składnia class

Od czasu wprowadzenia składni class możesz definiować metody „klasowe” w czytelny sposób, ale pod spodem nadal działa ten sam mechanizm prototypów.

Przykład klasy:

class Person {
constructor(name) {
this.name = name;
}

sayHello() {
return `Cześć, jestem ${this.name}`;
}
}

Sprawdzenie powiązania instancji z prototypem:

const jan = new Person('Jan');
console.log(Object.getPrototypeOf(jan) === Person.prototype); // true

W praktyce:

  • zamiast ręcznie manipulować prototype i __proto__,
  • używasz class, extends i super,
  • ale zrozumienie różnicy między prototype i __proto__ ułatwia debugowanie i analizę starszego lub transpile’owanego kodu.

11. Krótkie „ściągi” w formie mentalnych modeli

prototype (na funkcji): „to prototyp obiektu, który powstanie, gdy ktoś wywoła new TąFunkcją()” – używaj przy projektowaniu typu/„klasy”.

__proto__ (na obiekcie): „z tego prototypu pochodzę – tu szukaj moich brakujących właściwości” – używaj raczej Object.getPrototypeOf do diagnozy instancji.

Z praktycznego punktu widzenia w nowoczesnym kodzie webowym warto pamiętać o następujących priorytetach:

  • do implementowania logiki: class lub prototype,
  • do introspekcji i tworzenia obiektów: Object.getPrototypeOf i Object.create,
  • do __proto__ odnoś się głównie przy czytaniu starszych tutoriali, bibliotek lub logów w konsoli.