Programowanie obiektowe (OOP) to paradygmat, w którym aplikację buduje się z obiektów, a w JavaScripcie tworzy się je za pomocą literałów, Object.create(), funkcji konstruktorów wywoływanych operatorem new oraz słowa kluczowego class. Pod spodem class to wciąż funkcja oparta na prototypach (prototype): to one pozwalają współdzielić metody między obiektami zamiast powielać kod w każdym z nich. 57-minutowe nagranie omawia też this w konstruktorach, dziedziczenie w ES6 za pomocą extends i super oraz łańcuchy prototypów.
Całe nagranie trwa 57 minut. Omawiam w nim programowanie obiektowe w JavaScripcie krok po kroku: od zwykłych obiektów, przez konstruktory i prototypy, aż po klasy i dziedziczenie. Każde zagadnienie z wideo znajdziesz poniżej jako osobny nagłówek z wyjaśnieniem i przykładem kodu, więc możesz od razu przejść do interesującego Cię fragmentu. Samo nagranie jest osadzone na końcu artykułu.
Czym jest programowanie obiektowe?
Programowanie obiektowe (ang. object oriented programming, OOP) to paradygmat, w którym aplikację budujesz z obiektów – jednostek łączących dane (właściwości) i zachowania (metody). Umiejętność sprawnego posługiwania się obiektami jest niezbędna do wytwarzania oprogramowania w JavaScripcie, bo niemal wszystko w tym języku jest obiektem lub zachowuje się jak obiekt.
Jakie są cechy programowania obiektowego?
Najważniejsze cechy OOP to dziedziczenie, abstrakcja, hermetyzacja (enkapsulacja) i polimorfizm. Każdą z nich omawiam na osobnych przykładach w artykule 4 filary programowania obiektowego. Tutaj skupiamy się na tym, jak w JavaScripcie technicznie tworzy się obiekty.
Jak tworzyć obiekty w JavaScript?
Jak tworzyć obiekty przy pomocy literałów?
Najprostszy sposób na stworzenie obiektu to literał, czyli zapis w klamrach {}:
const anna = {
name: 'Anna',
greet() {
console.log(`Cześć, jestem ${this.name}`);
}
};
const piotr = {
name: 'Piotr',
greet() {
console.log(`Cześć, jestem ${this.name}`);
}
};
anna.greet(); // Cześć, jestem Anna
piotr.greet(); // Cześć, jestem Piotr
Na czym polega problem powielania kodu w literałach?
Spójrz na powyższy przykład: metodę greet() musieliśmy zapisać dwa razy. Przy dziesięciu, a tym bardziej tysiącu obiektów kopiowanie tego samego kodu staje się nie do utrzymania, a każda zmiana wymaga poprawek w wielu miejscach. Rozwiązują to kolejne techniki.
Jak działa Object.create()?
Metoda Object.create() tworzy nowy obiekt i ustawia mu inny obiekt jako prototyp. Dzięki temu wspólną metodę możemy zapisać raz:
const person = {
greet() {
console.log(`Cześć, jestem ${this.name}`);
}
};
const anna = Object.create(person);
anna.name = 'Anna';
const piotr = Object.create(person);
piotr.name = 'Piotr';
anna.greet(); // Cześć, jestem Anna
piotr.greet(); // Cześć, jestem Piotr
Obiekty anna i piotr same nie mają metody greet(). Gdy JavaScript jej w nich nie znajdzie, szuka dalej w prototypie.
Czym jest proto w JavaScript?
proto to ukryty odnośnik obiektu do jego prototypu. W kodzie widać go jako __proto__, ale zalecany sposób odczytu to Object.getPrototypeOf():
console.log(Object.getPrototypeOf(anna) === person); // true
Do czego służy metoda .hasOwnProperty()?
Ponieważ obiekt widzi zarówno własne właściwości, jak i te odziedziczone po prototypie, potrzebujemy sposobu, żeby je rozróżnić. Służy do tego .hasOwnProperty() – zwraca true tylko wtedy, gdy właściwość należy bezpośrednio do obiektu:
console.log(anna.hasOwnProperty('name')); // true – własna właściwość
console.log(anna.hasOwnProperty('greet')); // false – pochodzi z prototypu
console.log('greet' in anna); // true – operator in sprawdza też prototypy
Czym są konstruktory w JavaScript?
Jak działa funkcja konstruktora?
Konstruktor to zwykła funkcja, której używamy do tworzenia obiektów. Umownie zapisujemy ją wielką literą i wywołujemy z operatorem new:
function Person(name, age) {
this.name = name;
this.age = age;
this.greet = function () {
console.log(`Cześć, jestem ${this.name}, mam ${this.age} lat`);
};
}
const anna = new Person('Anna', 28);
const piotr = new Person('Piotr', 35);
anna.greet(); // Cześć, jestem Anna, mam 28 lat
piotr.greet(); // Cześć, jestem Piotr, mam 35 lat
Jak działa this w konstruktorach?
Wewnątrz konstruktora słowo this wskazuje na nowo tworzony obiekt. Dlatego this.name = name oznacza: „zapisz w tworzonym teraz obiekcie właściwość name”. Każdy obiekt utworzony z new Person(...) ma więc własne this i własne dane.
Jak używać parametrów w konstruktorze?
Parametry (name, age) pozwalają nadać każdemu obiektowi inne wartości, bez kopiowania kodu. Ten sam konstruktor tworzy zarówno anna, jak i piotr, a różnią się tylko przekazanymi argumentami.
Jak tworzyć metody w ciele konstruktora?
Metodę możemy zapisać w konstruktorze, przypisując funkcję do this, tak jak zrobiliśmy to z this.greet. Każdy obiekt dostaje wtedy swoją kopię tej metody i ma dostęp do własnych danych przez this.
Co robi operator new?
Wywołanie new Person('Anna', 28) wykonuje po kolei kilka kroków:
- tworzy pusty obiekt,
- ustawia jego prototyp na
Person.prototype, - wywołuje funkcję
Person, przy czymthiswskazuje na ten nowy obiekt, - zwraca ten obiekt (chyba że funkcja sama zwróci inny obiekt).
Dlatego zapomnienie o new kończy się błędem: bez niego this nie wskazuje na nowy obiekt, a funkcja nic nie zwraca.
Na czym polega problem powielania kodu w konstruktorach?
Konstruktor rozwiązał problem z literałami, ale zostawił inny. Metoda greet jest tworzona od nowa dla każdego obiektu:
console.log(anna.greet === piotr.greet); // false – to dwie osobne funkcje w pamięci
Przy tysiącu obiektów mamy tysiąc kopii tej samej funkcji. Rozwiązaniem są prototypy.
Czym jest prototype w JavaScript?
Każda funkcja konstruktora ma właściwość prototype – obiekt, który staje się prototypem wszystkich obiektów utworzonych przez new. Metodę zapisujemy w nim raz, a wszystkie obiekty z niej korzystają:
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.greet = function () {
console.log(`Cześć, jestem ${this.name}, mam ${this.age} lat`);
};
const anna = new Person('Anna', 28);
const piotr = new Person('Piotr', 35);
console.log(anna.greet === piotr.greet); // true – jedna funkcja współdzielona przez wszystkie obiekty
Jak prototypy działają w praktyce na przykładzie .forEach()?
Metody tablic, takie jak .forEach(), nie są zapisane w każdej tablicy z osobna – leżą w Array.prototype. Możemy sami zbudować uproszczoną wersję tej metody:
Array.prototype.myForEach = function (callback) {
for (let i = 0; i < this.length; i++) {
callback(this[i], i, this);
}
};
[1, 2, 3].myForEach((number) => console.log(number * 2)); // 2, 4, 6
Dzięki temu rozumiesz, skąd tablica „bierze” swoje metody: z prototypu. To dobry przykład do nauki, ale w prawdziwych projektach nie rozszerzaj wbudowanych prototypów (takich jak Array.prototype), bo może to kolidować z innym kodem.
Czy funkcja strzałkowa może być konstruktorem?
Nie. Funkcje strzałkowe nie mają własnego this ani właściwości prototype, więc nie da się ich wywołać z new:
const Person = (name) => {
this.name = name;
};
const anna = new Person('Anna'); // TypeError: Person is not a constructor
Na konstruktory używamy więc zwykłych funkcji (function) albo klas.
Jak działa słowo kluczowe class?
Słowo class weszło do JavaScriptu wraz z ES6 i daje czytelniejszy zapis tego samego mechanizmu, który poznałeś wyżej.
Czy class to nadal funkcja i prototype?
Tak. class to wciąż funkcja i pod spodem korzysta z prototype. Możesz to łatwo sprawdzić:
class Person {
constructor(name) {
this.name = name;
}
greet() {
console.log(`Cześć, jestem ${this.name}`);
}
}
console.log(typeof Person); // 'function'
const anna = new Person('Anna');
const piotr = new Person('Piotr');
console.log(anna.greet === piotr.greet); // true – metoda leży w Person.prototype
Jak definiować właściwości w klasie: constructor()?
Właściwości ustawiamy w metodzie constructor(), używając this. Wywołuje się ona automatycznie przy każdym new Person(...), dokładnie tak jak funkcja konstruktora z poprzednich przykładów. W przykładzie wyżej właściwość name powstaje właśnie w ten sposób.
Jak definiować metody w klasie?
Metody zapisujemy bezpośrednio w ciele klasy, bez słowa function (np. greet() powyżej). Trafiają one do prototypu, więc – inaczej niż w konstruktorze funkcyjnym – nie są duplikowane w każdym obiekcie.
Do czego służy inicjalizator właściwości?
Zamiast przypisywać każdą właściwość w konstruktorze, możesz zadeklarować ją bezpośrednio w ciele klasy – to tzw. inicjalizator właściwości (ang. class field):
class Person {
role = 'user'; // inicjalizator właściwości
constructor(name) {
this.name = name;
}
}
const anna = new Person('Anna');
console.log(anna.role); // 'user'
Taka właściwość trafia do każdego utworzonego obiektu, w odróżnieniu od metod, które są współdzielone przez prototyp.
Jak ukryć pola i metody w klasie: słowo kluczowe #?
Dostęp do pól i metod tylko z wnętrza klasy zapewnia prefiks #, czyli pola i metody prywatne (ES2022). Próba odczytania takiego pola z zewnątrz kończy się błędem:
class Account {
#balance = 0; // pole prywatne
deposit(amount) {
this.#balance += amount;
}
getBalance() {
return this.#balance;
}
}
const account = new Account();
account.deposit(100);
console.log(account.getBalance()); // 100
console.log(account.#balance); // SyntaxError: Private field '#balance' must be declared in an enclosing class
Ukrywanie szczegółów implementacji to właśnie hermetyzacja (enkapsulacja) – jeden z filarów OOP opisanych w artykule 4 filary programowania obiektowego.
Jak działa dziedziczenie w JavaScript?
Jak działa dziedziczenie w ES6: class i extends?
Dziedziczenie w klasach zapisujemy słowem extends. Klasa potomna przejmuje wszystkie właściwości i metody klasy nadrzędnej:
class Person {
constructor(name) {
this.name = name;
}
greet() {
console.log(`Cześć, jestem ${this.name}`);
}
}
class Developer extends Person {
constructor(name, language) {
super(name); // wywołanie konstruktora rodzica
this.language = language;
}
greet() {
super.greet(); // wywołanie metody rodzica
console.log(`Piszę w ${this.language}`);
}
}
const anna = new Developer('Anna', 'JavaScript');
anna.greet();
// Cześć, jestem Anna
// Piszę w JavaScript
Jak działa super i odwołanie do rodzica?
super pozwala odwołać się do klasy nadrzędnej na dwa sposoby: super(...) wywołuje konstruktor rodzica, a super.greet() – jego metodę. Warto zapamiętać dwie rzeczy:
- w konstruktorze klasy potomnej
super()musisz wywołać, zanim użyjeszthis– w przeciwnym razie dostanieszReferenceError, super.greet()pozwala rozszerzyć zachowanie rodzica, zamiast je w całości nadpisywać.
Czym jest łańcuch prototypów?
Gdy odwołujesz się do właściwości obiektu, JavaScript szuka jej najpierw w nim samym, potem w jego prototypie, w prototypie prototypu i tak dalej – aż do końca. To właśnie łańcuch prototypów (ang. prototype chain). Dla klasy Developer z poprzedniego przykładu wygląda on tak:
console.log(Object.getPrototypeOf(anna) === Developer.prototype); // true
console.log(Object.getPrototypeOf(Developer.prototype) === Person.prototype); // true
console.log(Object.getPrototypeOf(Person.prototype) === Object.prototype); // true
console.log(Object.getPrototypeOf(Object.prototype)); // null – koniec łańcucha
console.log(anna instanceof Developer); // true
console.log(anna instanceof Person); // true
Jeśli właściwości nie znajdzie w żadnym z elementów łańcucha, zwróci undefined. To mechanizm stojący za dziedziczeniem w JavaScripcie, a extends jest jedynie wygodniejszym zapisem tego samego.
Czym różni się dziedziczenie prototypowe od klasycznego?
To pytanie często pada na rozmowach rekrutacyjnych. W językach takich jak Java czy C# klasa jest planem – obiekty powstają jako jego kopie i dziedziczenie dotyczy klas. W JavaScripcie obiekty dziedziczą bezpośrednio po innych obiektach przez łańcuch prototypów, a class jest jedynie wygodniejszym zapisem tego mechanizmu:
| Dziedziczenie klasyczne (np. Java) | Dziedziczenie prototypowe (JavaScript) | |
|---|---|---|
| Kto dziedziczy po kim | klasa po klasie | obiekt po obiekcie (prototypie) |
| Czym jest klasa | osobną strukturą, planem obiektu | funkcją i składnią nad prototypami |
| Struktura po utworzeniu | zwykle niezmienna | można ją zmieniać w trakcie działania programu |
| Wyszukiwanie metod | zdefiniowane w czasie kompilacji | szukanie w łańcuchu prototypów w czasie działania |
Dlatego zrozumienie prototypów (opisanych wyżej) jest najlepszym sposobem, by naprawdę rozumieć klasy w JavaScripcie, zamiast traktować je jako „magię” znaną z innych języków.
💬 Rada mentora: Dobrze widać to na przykładzie dwóch popularnych narzędzi. W Reakcie kiedyś pisało się komponenty jako klasy (class Button extends React.Component), a dziś piszemy zwykłe funkcje, a zachowanie współdzielimy przez hooki, bez żadnego extends. Ciężar przeniósł się z dziedziczenia na kompozycję, czyli składanie większych rzeczy z mniejszych, niezależnych klocków. Jest ona lżejsza, bo nie wiąże klas w hierarchię, w której zmiana rodzica rusza wszystkie dzieci.
Angular idzie inną drogą: komponenty to nadal klasy i dziedziczenie jest tam w pełni wspierane, więc często spotkasz klasy bazowe współdzielące logikę. Równolegle framework dostarcza jednak własne narzędzia do kompozycji, jak inject() czy dyrektywy hosta. Wniosek jest taki: dziedziczenie znać trzeba, bo stoi za mechanizmem klas i spotkasz je w kodzie, ale nowy kod coraz częściej piszemy przez kompozycję. Zanim dodasz kolejne extends, zapytaj, czy nie da się tego złożyć z mniejszych elementów.
Obejrzyj całe nagranie
Poniżej znajdziesz pełny, 57-minutowy webinar, w którym pokazuję wszystkie powyższe zagadnienia na żywo.
Co dalej?
Znasz już techniczne podstawy OOP w JavaScripcie. Teraz warto:
- zrozumieć zasady, które za tym stoją, w artykule 4 filary programowania obiektowego,
- nauczyć się rysować relacje między klasami w artykule Podstawy UML – diagramy klas,
- sprawdzić, czy jesteś gotowy na dalszą naukę: Czy jestem gotowy na OOP?,
- pogłębić wiedzę w praktyce, tworząc realne projekty w ścieżce React, TypeScript i Next.js.


