Temat 26 · 2 × 45 min
Klasy i programowanie obiektowe
W lekcji 9 tworzyłeś obiekty literałami, a w lekcji 23 – funkcjami fabrykującymi. To działa, dopóki obiektów jest kilka. Gdy aplikacja ma pięćdziesiąt produktów w koszyku, każdy z tą samą metodą cenaBrutto(), potrzebujesz szablonu: jednego miejsca, które mówi, jak wygląda i co umie każdy produkt. Tym szablonem jest klasa (class). Klasy to także język, którym mówi INF.04 (C++, Java, Python) i każda większa biblioteka JavaScript – warto je rozumieć, a nie tylko kopiować.
Od obiektu do klasy
Zacznijmy od tego, co znasz. Trzech uczniów, każdy z imieniem, ocenami i metodą liczącą średnią:
function stworzUcznia(imie, oceny) {
return {
imie,
oceny,
srednia() {
const suma = this.oceny.reduce((acc, o) => acc + o, 0);
return suma / this.oceny.length;
},
};
}
const ola = stworzUcznia('Ola', [5, 4, 5]);
const bartek = stworzUcznia('Bartek', [3, 4]);
console.log(ola.srednia()); // 4.666…
console.log(bartek.srednia()); // 3.5
console.log(ola.srednia === bartek.srednia); // false – każdy obiekt ma WŁASNĄ kopię funkcjiFunkcja fabrykująca działa, ale ostatnia linia pokazuje koszt: każdy obiekt dostaje własną kopię każdej metody. Przy tysiącu obiektów to tysiąc identycznych funkcji w pamięci. Klasa rozwiązuje to elegancko:
class Uczen {
// konstruktor uruchamia się przy każdym `new Uczen(...)`
constructor(imie, oceny) {
this.imie = imie;
this.oceny = oceny;
}
// metoda – bez słowa `function`, bez przecinków między metodami
srednia() {
const suma = this.oceny.reduce((acc, o) => acc + o, 0);
return suma / this.oceny.length;
}
opis() {
return `${this.imie}: średnia ${this.srednia().toFixed(2)}`;
}
}
const ola = new Uczen('Ola', [5, 4, 5]);
const bartek = new Uczen('Bartek', [3, 4]);
console.log(ola.opis()); // Ola: średnia 4.67
console.log(bartek.opis()); // Bartek: średnia 3.50
console.log(ola.srednia === bartek.srednia); // true – jedna wspólna metoda
console.log(typeof Uczen); // function – klasa to specjalny rodzaj funkcjiSłownictwo, które będzie się powtarzać:
| Pojęcie | Znaczenie | W kodzie |
|---|---|---|
| klasa (class) | szablon: jakie pola i metody ma każdy obiekt | class Uczen { … } |
| instancja (instance) | konkretny obiekt zrobiony z klasy | const ola = new Uczen(…) |
| konstruktor (constructor) | metoda uruchamiana przez new, ustawia pola | constructor(imie) { this.imie = imie; } |
| pole (field, właściwość) | dane obiektu | this.imie, this.oceny |
| metoda (method) | funkcja należąca do klasy | srednia() { … } |
this | w metodzie: obiekt, na którym ją wywołano | ola.opis() → this === ola |
Zasady składni, o których łatwo zapomnieć: nazwy klas piszemy WielkąLiterą (Uczen, KontoBankowe), między metodami nie ma przecinków (to nie literał obiektu), a kod w klasie zawsze działa w trybie ścisłym ('use strict'), nawet jeśli reszta pliku nie.
Co wypisze ten kod?
class Licznik {
constructor() {
this.n = 0;
}
zwieksz() {
this.n++;
return this;
}
}
const l = new Licznik();
l.zwieksz().zwieksz().zwieksz();
console.log(l.n);Co jest pod spodem: prototypy w zarysie
Skąd ola.srednia === bartek.srednia daje true? Klasa nie kopiuje metod do obiektów. Metody lądują w jednym obiekcie – Uczen.prototype – a każda instancja ma ukryty odnośnik do niego. Gdy piszesz ola.srednia(), JavaScript sprawdza najpierw własne właściwości ola (jest tam imie, oceny), nie znajduje srednia, więc idzie po odnośniku do Uczen.prototype i tam ją znajduje.
class Uczen {
constructor(imie) {
this.imie = imie;
}
przedstawSie() {
return `Jestem ${this.imie}`;
}
}
const ola = new Uczen('Ola');
console.log(Object.getPrototypeOf(ola) === Uczen.prototype); // true
console.log(Object.hasOwn(ola, 'imie')); // true – własne pole
console.log(Object.hasOwn(ola, 'przedstawSie')); // false – metoda jest w prototypie
console.log('przedstawSie' in ola); // true – `in` szuka też w prototypach
console.log(ola.toString()); // [object Object] – z Object.prototype, dwa piętra wyżej
console.log(ola.constructor === Uczen); // trueNa tym etapie to wystarczy: klasa = cukier składniowy nad prototypami. Dziedziczenie, o którym będzie za chwilę, to po prostu dłuższy łańcuch takich odnośników. Jeśli kiedyś zobaczysz stary kod z function Uczen() {…} i Uczen.prototype.srednia = function () {…} – to ta sama maszyna bez cukru.
Gettery i settery
Czasem chcesz, żeby coś wyglądało jak pole (ola.srednia, bez nawiasów), ale było liczone na bieżąco. Albo żeby przypisanie (konto.saldo = -100) przechodziło przez kontrolę. Do tego są akcesory: get i set.
class Prostokat {
constructor(a, b) {
this.a = a;
this.b = b;
}
// getter – czytasz jak pole, liczy się przy każdym odczycie
get pole() {
return this.a * this.b;
}
// setter – przypisanie przechodzi przez funkcję
set bok(wartosc) {
if (wartosc <= 0) {
throw new RangeError('Bok musi być dodatni');
}
this.a = wartosc;
this.b = wartosc; // kwadrat
}
}
const p = new Prostokat(3, 4);
console.log(p.pole); // 12 – bez nawiasów!
p.bok = 5; // uruchamia setter
console.log(p.pole); // 25
console.log(typeof p.pole); // number – getter zwraca liczbę, nie funkcjęJaki kod należy wpisać w miejsce kropek, aby setter poprawnie ustawiał język obiektu?
const kurs = {
jezyk: 'JavaScript',
set j(nazwa) {
...........
}
};
kurs.j = 'Python';
console.log(kurs.jezyk); // PythonPola klasy i pola prywatne #
Pola nie muszą powstawać w konstruktorze. Możesz je zadeklarować bezpośrednio w ciele klasy – z wartością początkową albo bez. A jeśli poprzedzisz nazwę krzyżykiem #, pole staje się prywatne: dostępne tylko wewnątrz klasy, niewidoczne z zewnątrz.
class KontoBankowe {
// pola publiczne z wartością domyślną
waluta = 'PLN';
historia = [];
// pole prywatne – TYLKO kod wewnątrz klasy ma do niego dostęp
#saldo = 0;
constructor(wlasciciel) {
this.wlasciciel = wlasciciel;
}
wplac(kwota) {
this.#sprawdzKwote(kwota);
this.#saldo += kwota;
this.historia.push(`+${kwota}`);
}
wyplac(kwota) {
this.#sprawdzKwote(kwota);
if (kwota > this.#saldo) throw new Error('Brak środków');
this.#saldo -= kwota;
this.historia.push(`-${kwota}`);
}
get saldo() {
return this.#saldo; // odczyt jest dozwolony, ale tylko przez getter
}
// metoda prywatna
#sprawdzKwote(kwota) {
if (typeof kwota !== 'number' || kwota <= 0) throw new RangeError('Kwota musi być dodatnia');
}
}
const konto = new KontoBankowe('Ola');
konto.wplac(200);
konto.wyplac(50);
console.log(konto.saldo); // 150
console.log(konto.historia); // ["+200", "-50"]
konto.saldo = 1000000; // getter bez settera: przypisanie jest ignorowane (w trybie ścisłym: TypeError)
console.log(konto.saldo); // 150 – dalej 150
console.log(Object.keys(konto)); // ["waluta", "historia", "wlasciciel"] – #saldo niewidoczne
console.log(JSON.stringify(konto)); // {"waluta":"PLN","historia":["+200","-50"],"wlasciciel":"Ola"}Próba dostępu z zewnątrz to błąd składni – nie undefined, nie TypeError, tylko odmowa uruchomienia całego skryptu:
class Sejf {
#kod = '1234';
}
const s = new Sejf();
console.log(s.#kod); // SyntaxError: Private field '#kod' must be declared in an enclosing classKiedy używać prywatności? Gdy pole ma niezmienniki, których nikt z zewnątrz nie powinien łamać (saldo nie może być ujemne, indeks slajdu musi być w zakresie), albo gdy to szczegół implementacji, który chcesz móc zmienić bez psucia reszty kodu. Stara konwencja _saldo (podkreślnik) była tylko umową między programistami – #saldo jest pilnowane przez silnik.
static – to, co należy do klasy, nie do obiektu
Zwykła metoda potrzebuje instancji (ola.srednia()). Czasem jednak funkcja dotyczy klasy jako całości: licznik utworzonych obiektów, metoda porównująca dwie instancje, fabryka tworząca obiekt z JSON-a. Takie składowe oznaczasz static i wywołujesz na klasie: Uczen.zJson(…).
class Temperatura {
static ileUtworzono = 0; // pole statyczne – jedno dla całej klasy
static JEDNOSTKA = '°C';
constructor(stopnie) {
this.stopnie = stopnie;
Temperatura.ileUtworzono++; // uwaga: przez nazwę klasy, nie this
}
// metoda statyczna – alternatywny „konstruktor”
static zFahrenheita(f) {
return new Temperatura(((f - 32) * 5) / 9);
}
// porównanie dwu instancji – nie należy do żadnej z nich
static porownaj(a, b) {
return a.stopnie - b.stopnie;
}
toString() {
return `${this.stopnie.toFixed(1)}${Temperatura.JEDNOSTKA}`;
}
}
const t1 = new Temperatura(21.5);
const t2 = Temperatura.zFahrenheita(100);
console.log(String(t2)); // 37.8°C
console.log(Temperatura.ileUtworzono); // 2
console.log([t2, t1].sort(Temperatura.porownaj).map(String)); // ["21.5°C", "37.8°C"]
console.log(t1.ileUtworzono); // undefined – statyczne nie ma na instancjiMetoda toString() to przykład nadpisania metody z Object.prototype: od teraz String(t2), konkatenacja i template string używają twojej wersji. Podobnie toJSON() decyduje, co trafi do JSON.stringify – przydatne, gdy zapisujesz obiekty klas do localStorage (lekcja 25), a potem odtwarzasz je metodą statyczną zJson.
Co wypisze ten kod?
class Gra {
static maksGraczy = 4;
constructor(nazwa) {
this.nazwa = nazwa;
}
}
const g = new Gra('Chińczyk');
console.log(g.maksGraczy, Gra.maksGraczy);Dziedziczenie: extends i super
Klasy można budować jedna na drugiej. class Student extends Uczen oznacza: Student ma wszystko, co Uczen, plus własne dodatki. Klasę bazową nazywamy rodzicem (superclass), nową – dzieckiem (subclass).
class Pracownik {
constructor(imie, stawka) {
this.imie = imie;
this.stawka = stawka; // zł za godzinę
}
wynagrodzenie(godziny) {
return this.stawka * godziny;
}
opis() {
return `${this.imie} (${this.constructor.name})`;
}
}
class Kierownik extends Pracownik {
constructor(imie, stawka, premia) {
super(imie, stawka); // 1. NAJPIERW konstruktor rodzica – tworzy this
this.premia = premia; // 2. potem własne pola
}
// nadpisanie metody (override) z wykorzystaniem wersji rodzica
wynagrodzenie(godziny) {
return super.wynagrodzenie(godziny) + this.premia;
}
// nowa metoda, której Pracownik nie ma
deleguj(zadanie) {
return `${this.imie} deleguje: ${zadanie}`;
}
}
const ania = new Pracownik('Ania', 40);
const marek = new Kierownik('Marek', 60, 1500);
console.log(ania.wynagrodzenie(160)); // 6400
console.log(marek.wynagrodzenie(160)); // 11100 = 9600 + 1500
console.log(marek.opis()); // Marek (Kierownik) – odziedziczone, this.constructor.name = klasa dziecka
console.log(marek.deleguj('raport')); // Marek deleguje: raport
console.log(marek instanceof Kierownik); // true
console.log(marek instanceof Pracownik); // true – dziecko „jest też” rodzicem
console.log(ania instanceof Kierownik); // false
console.log(Object.getPrototypeOf(Kierownik.prototype) === Pracownik.prototype); // true – dłuższy łańcuchZasady, których silnik pilnuje bardzo surowo:
- Jeśli klasa dziecka ma własny
constructor, musi wywołaćsuper(...)i to przed pierwszym użyciemthis. Inaczej:ReferenceError. - Jeśli dziecko nie ma konstruktora, dostaje domyślny:
constructor(...args) { super(...args); }– więc najczęściej go po prostu nie piszesz. super.metoda()wywołuje wersję rodzica – tak dokładasz coś do istniejącego zachowania zamiast kopiować kod.
class Zwierze {
constructor(imie) {
this.imie = imie;
}
}
class Pies extends Zwierze {
constructor(imie, rasa) {
this.rasa = rasa; // ReferenceError: Must call super constructor in derived class before accessing 'this'
super(imie);
}
}
new Pies('Reksio', 'kundel');Polimorfizm w praktyce
Największa korzyść z dziedziczenia: kod, który pracuje z rodzicem, działa z każdym dzieckiem i sam wybiera właściwą metodę.
class Figura {
constructor(nazwa) {
this.nazwa = nazwa;
}
pole() {
throw new Error(`${this.constructor.name} musi zaimplementować pole()`);
}
opis() {
return `${this.nazwa}: ${this.pole().toFixed(2)} cm²`;
}
}
class Kolo extends Figura {
constructor(r) {
super('koło');
this.r = r;
}
pole() {
return Math.PI * this.r ** 2;
}
}
class Prostokat extends Figura {
constructor(a, b) {
super('prostokąt');
this.a = a;
this.b = b;
}
pole() {
return this.a * this.b;
}
}
const figury = [new Kolo(1), new Prostokat(2, 3), new Kolo(2)];
// ta pętla nie wie, z jaką figurą ma do czynienia – i nie musi
for (const f of figury) console.log(f.opis());
// koło: 3.14 cm²
// prostokąt: 6.00 cm²
// koło: 12.57 cm²
const suma = figury.reduce((acc, f) => acc + f.pole(), 0);
console.log(suma.toFixed(2)); // 21.71Co wypisze ten kod?
class A {
kto() { return 'A'; }
przedstaw() { return 'Jestem ' + this.kto(); }
}
class B extends A {
kto() { return 'B'; }
}
console.log(new B().przedstaw());this w metodach przekazywanych dalej
Najczęstszy błąd przy łączeniu klas z DOM-em. Metoda klasy to zwykła funkcja: jeśli ją oderwiesz od obiektu i przekażesz gdzie indziej (do addEventListener, setTimeout, map), this przestaje wskazywać instancję. W klasach (tryb ścisły) staje się undefined.
class Licznik {
constructor() {
this.n = 0;
}
zwieksz() {
this.n++;
console.log('n =', this.n);
}
}
const l = new Licznik();
l.zwieksz(); // n = 1 – wywołanie przez kropkę: this = l
const oderwana = l.zwieksz; // sama funkcja, bez obiektu
oderwana(); // TypeError: Cannot read properties of undefined (reading 'n')Dokładnie to się dzieje w przycisk.addEventListener('click', licznik.zwieksz) – przeglądarka wywoła funkcję z this ustawionym na przycisk, a nie na licznik. Trzy sprawdzone rozwiązania:
class Licznik {
n = 0;
// ROZWIĄZANIE 3: pole klasy z funkcją strzałkową – strzałka bierze this z miejsca definicji,
// czyli z instancji. Każdy obiekt ma własną kopię tej funkcji (koszt jak w fabryce), ale this nie ginie.
zwiekszStrzalka = () => {
this.n++;
};
zwieksz() {
this.n++;
}
}
const l = new Licznik();
// ROZWIĄZANIE 1: opakuj w strzałkę w miejscu przekazania
const przezStrzalke = () => l.zwieksz();
przezStrzalke();
// ROZWIĄZANIE 2: bind tworzy kopię funkcji z przypiętym this
const przypieta = l.zwieksz.bind(l);
przypieta();
const oderwana = l.zwiekszStrzalka;
oderwana(); // działa dzięki rozwiązaniu 3
console.log(l.n); // 3Zasada praktyczna: w addEventListener i setTimeout zawsze przekazuj strzałkę () => obiekt.metoda(). Jeśli chcesz później usunąć nasłuchiwacz przez removeEventListener, potrzebujesz tej samej referencji – wtedy użyj pola-strzałki (rozwiązanie 3) albo zapisz wynik bind w polu w konstruktorze.
Klasa jako komponent interfejsu
Klasy najbardziej opłacają się tam, gdzie stan i wygląd muszą iść w parze: licznik, slider, lista zadań. Instancja trzyma stan (prywatnie), a metoda render() przerysowuje tylko swój fragment DOM. Taki wzorzec to w uproszczeniu to, co robią komponenty w React (lekcja 35).
<div class="liczniki">
<div id="l1"></div>
<div id="l2"></div>
<div id="l3"></div>
</div>.liczniki { display: flex; gap: 16px; flex-wrap: wrap; }
.licznik { border: 2px solid #15171c; border-radius: 10px; padding: 10px 14px; display: flex; gap: 8px; align-items: center; }
.licznik button { font-size: 18px; width: 36px; height: 36px; border-radius: 8px; border: 1px solid #15171c; background: #f2d338; cursor: pointer; }
.licznik output { min-width: 2ch; text-align: center; font-weight: 700; font-size: 20px; }class Licznik {
#wartosc;
#min;
#max;
#out; // element <output>, do którego piszemy
constructor(kontener, { start = 0, min = 0, max = 10 } = {}) {
this.#wartosc = start;
this.#min = min;
this.#max = max;
kontener.className = 'licznik';
kontener.innerHTML = `
<button type="button" data-krok="-1" aria-label="mniej">−</button>
<output>${start}</output>
<button type="button" data-krok="1" aria-label="więcej">+</button>
`;
this.#out = kontener.querySelector('output');
// jeden nasłuchiwacz na kontener (delegacja – lekcja 27); strzałka zachowuje this
kontener.addEventListener('click', (e) => {
const btn = e.target.closest('button');
if (btn) this.zmien(Number(btn.dataset.krok));
});
}
get wartosc() {
return this.#wartosc;
}
zmien(oIle) {
const nowa = this.#wartosc + oIle;
if (nowa < this.#min || nowa > this.#max) return; // niezmiennik pilnowany w jednym miejscu
this.#wartosc = nowa;
this.#render();
}
#render() {
this.#out.textContent = this.#wartosc;
this.#out.style.color = this.#wartosc === this.#max ? 'crimson' : 'inherit';
}
}
const liczniki = [
new Licznik(document.getElementById('l1')),
new Licznik(document.getElementById('l2'), { start: 5, max: 5 }),
new Licznik(document.getElementById('l3'), { min: -3, max: 3 }),
];
console.log('Utworzono liczników:', liczniki.length);Zwróć uwagę: trzy instancje, zero zmiennych globalnych, każda pilnuje własnego zakresu. Zrobienie tego samego trzema zestawami funkcji i zmiennych (jak w lekcji 17) wymagałoby trzykrotnie więcej kodu – i łatwo byłoby pomylić, który licznik jest który.
Klasa, fabryka czy zwykły obiekt?
Klasy nie są odpowiedzią na wszystko. Krótki przewodnik:
| Sytuacja | Użyj | Dlaczego |
|---|---|---|
| Jeden obiekt konfiguracji, rekord z API, „torba na dane” | literał { … } | Zero ceremonii; klasa byłaby na wyrost |
| Wiele obiektów o tym samym kształcie i metodach, zwłaszcza ze stanem do pilnowania | class | Wspólne metody, #prywatne, instanceof, czytelne dla innych języków |
Potrzebujesz prawdziwej prywatności bez #, albo chcesz zwracać różne kształty | funkcja fabrykująca + domknięcie | Elastyczna, ale metody się kopiują i nie ma instanceof |
Zestaw powiązanych funkcji bez stanu (np. formatujDate, parsujDate) | zwykłe funkcje w module (lekcja 32) | Klasa bez stanu to tylko przestrzeń nazw – static wszędzie to sygnał, że klasa nie jest potrzebna |
Ściąga
| Składnia | Co robi | Przykład |
|---|---|---|
class Nazwa { … } | definiuje klasę (tryb ścisły, bez przecinków między metodami) | class Uczen { … } |
constructor(a, b) { this.a = a; } | uruchamiany przez new, ustawia pola | new Uczen('Ola', [5]) |
metoda() { … } | metoda wspólna dla instancji (ląduje w Nazwa.prototype) | uczen.srednia() |
pole = wartosc; | pole publiczne z wartością początkową | historia = []; |
#pole, #metoda() | prywatne – dostęp tylko w klasie; z zewnątrz SyntaxError | this.#saldo += kwota |
get x() { … } | odczyt jak pola, liczony na bieżąco | p.pole (bez nawiasów) |
set x(v) { … } | przypisanie przechodzi przez funkcję; zapisuj do innego pola | set bok(v) { this.a = v; } |
static x, static f() {} | należy do klasy, nie do instancji | Temperatura.zFahrenheita(100) |
class B extends A | dziedziczenie: B ma wszystko z A | class Kierownik extends Pracownik |
super(...) | konstruktor rodzica – pierwszy w konstruktorze dziecka | super(imie, stawka) |
super.metoda() | wersja metody z rodzica | super.wynagrodzenie(h) + this.premia |
obj instanceof Klasa | czy obiekt pochodzi z klasy (lub jej dzieci) | marek instanceof Pracownik |
toString(), toJSON() | nadpisujesz, by sterować String(obj) i JSON.stringify(obj) | toJSON() { return { … }; } |
() => this.metoda() | zachowanie this w callbacku | btn.addEventListener('click', () => l.zwieksz()) |
Object.getPrototypeOf(obj) | prototyp obiektu (=== Klasa.prototype) | sprawdzenie, co jest pod spodem |
Zadania
Produkt z ceną brutto
Napisz klasę Produkt z konstruktorem (nazwa, cenaNetto, vat = 23) i:
- getterem
cenaBruttozwracającym cenę z VAT zaokrągloną do 2 miejsc (jako liczbę), - metodą
toString()zwracającą np.Chleb – 5.25 zł, - statyczną metodą
najtanszy(lista)zwracającą produkt o najniższej cenie brutto.
Sprawdź na trzech produktach: chleb 5,00 (VAT 5 %), laptop 3000 (23 %), książka 40 (5 %).
Pokaż rozwiązanie
class Produkt {
constructor(nazwa, cenaNetto, vat = 23) {
this.nazwa = nazwa;
this.cenaNetto = cenaNetto;
this.vat = vat;
}
get cenaBrutto() {
// Number(...toFixed(2)) zaokrągla i wraca do liczby
return Number((this.cenaNetto * (1 + this.vat / 100)).toFixed(2));
}
toString() {
return `${this.nazwa} – ${this.cenaBrutto.toFixed(2)} zł`;
}
static najtanszy(lista) {
return lista.reduce((min, p) => (p.cenaBrutto < min.cenaBrutto ? p : min));
}
}
const produkty = [new Produkt('Chleb', 5, 5), new Produkt('Laptop', 3000), new Produkt('Książka', 40, 5)];
for (const p of produkty) console.log(String(p));
// Chleb – 5.25 zł
// Laptop – 3690.00 zł
// Książka – 42.00 zł
console.log('Najtańszy:', Produkt.najtanszy(produkty).nazwa); // Najtańszy: ChlebKonto z limitem i historią
Rozbuduj klasę KontoBankowe z lekcji:
- saldo i limit debetu (
#saldo,#limit) są prywatne; limit podajesz w konstruktorze (domyślnie 0), wyplac(kwota)pozwala zejść poniżej zera, ale nie poniżej-limit; przy przekroczeniu rzucaErrorz komunikatemPrzekroczono limit debetu,- getter
saldoi getterwDebecie(true, gdy saldo ujemne), - każda operacja dopisuje do
historiaobiekt{ typ: 'wplata' | 'wyplata', kwota, saldoPo }, - metoda
toJSON()zwraca{ wlasciciel, limit, saldo, historia }, abyJSON.stringify(konto)zawierał saldo mimo prywatności, - statyczna
zJson(tekst)odtwarza konto z takiego JSON-a (saldo ustaw bez dopisywania do historii – pomyśl, jak to zrobić bez publicznego settera).
Pokaż rozwiązanie
class KontoBankowe {
#saldo = 0;
#limit;
historia = [];
constructor(wlasciciel, limit = 0) {
this.wlasciciel = wlasciciel;
this.#limit = limit;
}
wplac(kwota) {
this.#sprawdz(kwota);
this.#saldo += kwota;
this.historia.push({ typ: 'wplata', kwota, saldoPo: this.#saldo });
}
wyplac(kwota) {
this.#sprawdz(kwota);
if (this.#saldo - kwota < -this.#limit) throw new Error('Przekroczono limit debetu');
this.#saldo -= kwota;
this.historia.push({ typ: 'wyplata', kwota, saldoPo: this.#saldo });
}
get saldo() {
return this.#saldo;
}
get wDebecie() {
return this.#saldo < 0;
}
toJSON() {
return { wlasciciel: this.wlasciciel, limit: this.#limit, saldo: this.#saldo, historia: this.historia };
}
// metoda statyczna jest „wewnątrz klasy”, więc MOŻE pisać do #saldo nowej instancji
static zJson(tekst) {
const dane = JSON.parse(tekst);
const konto = new KontoBankowe(dane.wlasciciel, dane.limit);
konto.#saldo = dane.saldo;
konto.historia = dane.historia;
return konto;
}
#sprawdz(kwota) {
if (!(kwota > 0)) throw new RangeError('Kwota musi być dodatnia');
}
}
const konto = new KontoBankowe('Ola', 100);
konto.wplac(50);
konto.wyplac(120);
console.log(konto.saldo, konto.wDebecie); // -70 true
try {
konto.wyplac(50); // -120 < -100
} catch (e) {
console.log('Błąd:', e.message); // Błąd: Przekroczono limit debetu
}
const zapis = JSON.stringify(konto);
console.log(zapis); // {"wlasciciel":"Ola","limit":100,"saldo":-70,"historia":[…]}
const kopia = KontoBankowe.zJson(zapis);
console.log(kopia.saldo, kopia.historia.length, kopia instanceof KontoBankowe); // -70 2 trueBiblioteka książek z filtrowaniem
Zbuduj dwie klasy i interfejs:
Ksiazka(tytul, autor, rok)z polemwypozyczona = falsei metodąprzelacz()(zmienia stan na przeciwny),Bibliotekatrzyma tablicę książek (prywatnie), ma metodydodaj(ksiazka),szukaj(fraza)(po tytule lub autorze, bez rozróżniania wielkości liter),dostepne()irender(kontener, fraza = ''), która rysuje listę<li>z tytułem, autorem, rokiem i przyciskiem „Wypożycz”/„Zwróć”.
HTML: pole <input id="szukaj" placeholder="Szukaj…">, <ul id="lista"></ul>, <p id="info"></p> (liczba dostępnych). Pisanie w polu filtruje listę na żywo; kliknięcie przycisku przełącza stan i przerysowuje. Użyj jednego nasłuchiwacza na ul i atrybutu data-index.
Pokaż rozwiązanie
<input id="szukaj" placeholder="Szukaj po tytule lub autorze…">
<p id="info"></p>
<ul id="lista"></ul>#szukaj { width: 100%; padding: 8px; font-size: 16px; box-sizing: border-box; }
#lista { list-style: none; padding: 0; }
#lista li { display: flex; justify-content: space-between; align-items: center; gap: 12px; padding: 6px 0; border-bottom: 1px solid #ddd; }
#lista li.wypozyczona span { opacity: 0.5; text-decoration: line-through; }
#lista button { padding: 4px 10px; border-radius: 6px; border: 1px solid #15171c; background: #f2d338; cursor: pointer; }class Ksiazka {
wypozyczona = false;
constructor(tytul, autor, rok) {
this.tytul = tytul;
this.autor = autor;
this.rok = rok;
}
przelacz() {
this.wypozyczona = !this.wypozyczona;
}
}
class Biblioteka {
#ksiazki = [];
dodaj(ksiazka) {
this.#ksiazki.push(ksiazka);
return this; // łańcuchowanie: dodaj(…).dodaj(…)
}
szukaj(fraza) {
const f = fraza.trim().toLowerCase();
return this.#ksiazki.filter((k) => k.tytul.toLowerCase().includes(f) || k.autor.toLowerCase().includes(f));
}
dostepne() {
return this.#ksiazki.filter((k) => !k.wypozyczona);
}
render(kontener, fraza = '') {
const wyniki = this.szukaj(fraza);
kontener.innerHTML = wyniki
.map((k) => {
// data-index wskazuje pozycję w PEŁNEJ liście, nie w przefiltrowanej
const index = this.#ksiazki.indexOf(k);
return `<li class="${k.wypozyczona ? 'wypozyczona' : ''}">
<span>${k.tytul} – ${k.autor} (${k.rok})</span>
<button type="button" data-index="${index}">${k.wypozyczona ? 'Zwróć' : 'Wypożycz'}</button>
</li>`;
})
.join('');
if (wyniki.length === 0) kontener.innerHTML = '<li>Brak wyników</li>';
}
przelacz(index) {
this.#ksiazki[index].przelacz();
}
}
const biblioteka = new Biblioteka()
.dodaj(new Ksiazka('Wiedźmin. Ostatnie życzenie', 'Andrzej Sapkowski', 1993))
.dodaj(new Ksiazka('Solaris', 'Stanisław Lem', 1961))
.dodaj(new Ksiazka('Lalka', 'Bolesław Prus', 1890))
.dodaj(new Ksiazka('Cyberiada', 'Stanisław Lem', 1965));
const pole = document.getElementById('szukaj');
const lista = document.getElementById('lista');
const info = document.getElementById('info');
function odswiez() {
biblioteka.render(lista, pole.value);
info.textContent = `Dostępne: ${biblioteka.dostepne().length}`;
}
pole.addEventListener('input', odswiez);
lista.addEventListener('click', (e) => {
const btn = e.target.closest('button[data-index]');
if (!btn) return;
biblioteka.przelacz(Number(btn.dataset.index));
odswiez();
});
odswiez();Hierarchia figur z walidacją
Zaprojektuj klasę bazową Figura i trzy klasy pochodne: Kolo(r), Prostokat(a, b), Trojkat(a, b, c). Wymagania:
Figuramapole()iobwod(), które rzucająError('Nie zaimplementowano')– klasy pochodne je nadpisują,- konstruktor
Trojkatsprawdza nierówność trójkąta (każdy bok mniejszy od sumy pozostałych); jeśli nie –RangeError('Taki trójkąt nie istnieje'); pole licz ze wzoru Herona, Figura.porownajPola(a, b)to statyczny komparator dosort,- getter
nazwazwraca nazwę klasy małymi literami (this.constructor.name.toLowerCase()), toString()wFiguradaje np.kolo: pole 12.57, obwód 12.57(nazwa z getteranazwa) – klasy pochodne go nie nadpisują,- zbuduj tablicę z 4 figur (w tym jeden niepoprawny trójkąt złapany w
try…catch), posortuj po polu i wypisz.
Pokaż rozwiązanie
class Figura {
pole() {
throw new Error('Nie zaimplementowano');
}
obwod() {
throw new Error('Nie zaimplementowano');
}
get nazwa() {
return this.constructor.name.toLowerCase();
}
static porownajPola(a, b) {
return a.pole() - b.pole();
}
toString() {
// działa dla każdej klasy pochodnej – polimorfizm
return `${this.nazwa}: pole ${this.pole().toFixed(2)}, obwód ${this.obwod().toFixed(2)}`;
}
}
class Kolo extends Figura {
constructor(r) {
super();
this.r = r;
}
pole() {
return Math.PI * this.r ** 2;
}
obwod() {
return 2 * Math.PI * this.r;
}
}
class Prostokat extends Figura {
constructor(a, b) {
super();
this.a = a;
this.b = b;
}
pole() {
return this.a * this.b;
}
obwod() {
return 2 * (this.a + this.b);
}
}
class Trojkat extends Figura {
constructor(a, b, c) {
super();
if (a + b <= c || a + c <= b || b + c <= a) throw new RangeError('Taki trójkąt nie istnieje');
this.a = a;
this.b = b;
this.c = c;
}
obwod() {
return this.a + this.b + this.c;
}
pole() {
const p = this.obwod() / 2; // wzór Herona
return Math.sqrt(p * (p - this.a) * (p - this.b) * (p - this.c));
}
}
const figury = [new Kolo(2), new Prostokat(2, 3), new Trojkat(3, 4, 5)];
try {
figury.push(new Trojkat(1, 2, 10));
} catch (e) {
console.log(`${e.name}: ${e.message}`); // RangeError: Taki trójkąt nie istnieje
}
figury.sort(Figura.porownajPola);
for (const f of figury) console.log(String(f));
// prostokat: pole 6.00, obwód 10.00
// trojkat: pole 6.00, obwód 12.00
// kolo: pole 12.57, obwód 12.57
console.log(figury.every((f) => f instanceof Figura)); // trueKółko i krzyżyk ze stanem w klasie
Napisz klasę Gra dla kółka i krzyżyk:
- prywatne pola:
#plansza(tablica 9 pól,null/'X'/'O'),#ruch(kto teraz:'X'startuje),#zwyciezca; koniec gry (wygrana lub remis) wyliczaj getteremkoniec, - metoda
zagraj(indeks)– ignoruje ruch, gdy pole zajęte lub gra skończona; po ruchu sprawdza zwycięzcę (8 linii) i remis; zwracatrue, gdy ruch wykonano, afalse, gdy go zignorowano, reset(),- gettery
plansza(kopia tablicy, żeby nikt nie zmieniał stanu z zewnątrz) ikomunikat(Ruch: X,Wygrał O,Remis).
Interfejs: <div id="plansza"> z 9 przyciskami generowanymi w pętli, <p id="komunikat">, przycisk „Nowa gra”. Jeden nasłuchiwacz na planszy (delegacja).
Pokaż rozwiązanie
<p id="komunikat"></p>
<div id="plansza"></div>
<button id="reset" type="button">Nowa gra</button>#plansza { display: grid; grid-template-columns: repeat(3, 64px); gap: 6px; margin-bottom: 10px; }
#plansza button { height: 64px; font-size: 28px; font-weight: 700; border: 2px solid #15171c; border-radius: 8px; background: #fff; cursor: pointer; }
#plansza button:disabled { cursor: default; }
#plansza button.wygrana { background: #f2d338; }class Gra {
static LINIE = [
[0, 1, 2], [3, 4, 5], [6, 7, 8], // wiersze
[0, 3, 6], [1, 4, 7], [2, 5, 8], // kolumny
[0, 4, 8], [2, 4, 6], // przekątne
];
#plansza;
#ruch;
#zwyciezca;
#linia; // indeksy wygrywającej linii (do podświetlenia)
constructor() {
this.reset();
}
reset() {
this.#plansza = Array(9).fill(null);
this.#ruch = 'X';
this.#zwyciezca = null;
this.#linia = null;
}
get plansza() {
return [...this.#plansza]; // kopia – stan zmienia tylko zagraj()
}
get koniec() {
return this.#zwyciezca !== null || this.#plansza.every(Boolean);
}
get linia() {
return this.#linia;
}
get komunikat() {
if (this.#zwyciezca) return `Wygrał ${this.#zwyciezca}`;
if (this.koniec) return 'Remis';
return `Ruch: ${this.#ruch}`;
}
zagraj(indeks) {
if (this.koniec || this.#plansza[indeks] !== null) return false;
this.#plansza[indeks] = this.#ruch;
this.#sprawdzZwyciezce();
if (!this.koniec) this.#ruch = this.#ruch === 'X' ? 'O' : 'X';
return true;
}
#sprawdzZwyciezce() {
for (const [a, b, c] of Gra.LINIE) {
const p = this.#plansza;
if (p[a] && p[a] === p[b] && p[a] === p[c]) {
this.#zwyciezca = p[a];
this.#linia = [a, b, c];
return;
}
}
}
}
const gra = new Gra();
const planszaEl = document.getElementById('plansza');
const komunikatEl = document.getElementById('komunikat');
// 9 przycisków w pętli, każdy zna swój indeks
for (let i = 0; i < 9; i++) {
const btn = document.createElement('button');
btn.type = 'button';
btn.dataset.indeks = i;
planszaEl.append(btn);
}
function render() {
const pola = gra.plansza;
planszaEl.querySelectorAll('button').forEach((btn, i) => {
btn.textContent = pola[i] ?? '';
btn.disabled = pola[i] !== null || gra.koniec;
btn.classList.toggle('wygrana', gra.linia?.includes(i) ?? false);
});
komunikatEl.textContent = gra.komunikat;
}
planszaEl.addEventListener('click', (e) => {
const btn = e.target.closest('button[data-indeks]');
if (!btn) return;
if (gra.zagraj(Number(btn.dataset.indeks))) render();
});
document.getElementById('reset').addEventListener('click', () => {
gra.reset();
render();
});
render();Test powtórkowy
Test powtórkowy
Klasy, akcesory, pola prywatne, static i dziedziczenie. Jedna poprawna odpowiedź.
Który zapis poprawnie tworzy obiekt z klasy Samochod?
Co wypisze ten kod?
class Punkt {
constructor(x, y) {
this.x = x;
this.y = y;
}
get odleglosc() {
return Math.sqrt(this.x ** 2 + this.y ** 2);
}
}
const p = new Punkt(3, 4);
console.log(p.odleglosc);Które zdanie o polu #stan w klasie jest prawdziwe?
Co wypisze ten kod?
class Baza {
constructor() {
this.typ = 'baza';
}
}
class Pochodna extends Baza {
constructor() {
super();
this.typ = 'pochodna';
}
}
const o = new Pochodna();
console.log(o.typ, o instanceof Baza);Dlaczego ten kod rzuca błąd?
class Zwierze {
constructor(imie) { this.imie = imie; }
}
class Kot extends Zwierze {
constructor(imie) {
this.miauczy = true;
super(imie);
}
}
new Kot('Filemon');Co wypisze ten kod?
class Licznik {
static ile = 0;
constructor() {
Licznik.ile++;
}
}
new Licznik();
new Licznik();
const l = new Licznik();
console.log(Licznik.ile, l.ile);Co wypisze ten kod?
class Rodzic {
powitanie() { return 'Cześć'; }
}
class Dziecko extends Rodzic {
powitanie() { return super.powitanie() + '!'; }
}
console.log(new Dziecko().powitanie());Przycisk ma zwiększać licznik, ale po kliknięciu w konsoli pojawia się TypeError: Cannot read properties of undefined (reading 'n'). Który zapis naprawi problem?
class Licznik {
n = 0;
zwieksz() { this.n++; }
}
const licznik = new Licznik();
btn.addEventListener('click', licznik.zwieksz);Które stwierdzenie o klasach w JavaScript jest prawdziwe?
Co wypisze ten kod?
class Towar {
constructor(cena) { this.cena = cena; }
toString() { return 'Towar za ' + this.cena + ' zł'; }
}
const t = new Towar(20);
console.log('Koszyk: ' + t);