Przejdź do treści

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ą:

bez-klasy.js
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ę funkcji
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik

Funkcja 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:

klasa.js
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 funkcji
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik

Słownictwo, które będzie się powtarzać:

PojęcieZnaczenieW kodzie
klasa (class)szablon: jakie pola i metody ma każdy obiektclass Uczen { … }
instancja (instance)konkretny obiekt zrobiony z klasyconst ola = new Uczen(…)
konstruktor (constructor)metoda uruchamiana przez new, ustawia polaconstructor(imie) { this.imie = imie; }
pole (field, właściwość)dane obiektuthis.imie, this.oceny
metoda (method)funkcja należąca do klasysrednia() { … }
thisw metodzie: obiekt, na którym ją wywołanoola.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.

Sprawdź się

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.

Trzy pudełka połączone strzałkami: obiekt uczen z polami imie i oceny wskazuje na Uczen.prototype z metodami srednia i toString, który wskazuje na Object.prototype z hasOwnProperty, toString i valueOf, a ten na null
Rys. 1. Łańcuch prototypów: własne pola są w instancji, metody – jedne dla wszystkich – w Uczen.prototype.
prototypy.js
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); // true
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik

Na 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.

get-set.js
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ę
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik
Sprawdź się

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); // Python

Pola 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.

pola-prywatne.js
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"}
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik

Próba dostępu z zewnątrz to błąd składni – nie undefined, nie TypeError, tylko odmowa uruchomienia całego skryptu:

prywatne-blad.js
class Sejf {
  #kod = '1234';
}
const s = new Sejf();
console.log(s.#kod); // SyntaxError: Private field '#kod' must be declared in an enclosing class
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik

Kiedy 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(…).

static.js
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 instancji
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik

Metoda 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.

Sprawdź się

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).

extends.js
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ńcuch
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik

Zasady, których silnik pilnuje bardzo surowo:

  1. Jeśli klasa dziecka ma własny constructor, musi wywołać super(...) i to przed pierwszym użyciem this. Inaczej: ReferenceError.
  2. Jeśli dziecko nie ma konstruktora, dostaje domyślny: constructor(...args) { super(...args); } – więc najczęściej go po prostu nie piszesz.
  3. super.metoda() wywołuje wersję rodzica – tak dokładasz coś do istniejącego zachowania zamiast kopiować kod.
super-blad.js
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');
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik

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ę.

polimorfizm.js
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.71
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik
Sprawdź się

Co 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.

this-oderwane.js
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')
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik

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:

this-naprawa.js
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); // 3
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik

Zasada 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).

Komponent Licznik – trzy niezależne instancje
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);
Podgląd

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:

SytuacjaUżyjDlaczego
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 pilnowaniaclassWspólne metody, #prywatne, instanceof, czytelne dla innych języków
Potrzebujesz prawdziwej prywatności bez #, albo chcesz zwracać różne kształtyfunkcja fabrykująca + domknięcieElastyczna, 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ładniaCo robiPrzykł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 polanew 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 SyntaxErrorthis.#saldo += kwota
get x() { … }odczyt jak pola, liczony na bieżącop.pole (bez nawiasów)
set x(v) { … }przypisanie przechodzi przez funkcję; zapisuj do innego polaset bok(v) { this.a = v; }
static x, static f() {}należy do klasy, nie do instancjiTemperatura.zFahrenheita(100)
class B extends Adziedziczenie: B ma wszystko z Aclass Kierownik extends Pracownik
super(...)konstruktor rodzica – pierwszy w konstruktorze dzieckasuper(imie, stawka)
super.metoda()wersja metody z rodzicasuper.wynagrodzenie(h) + this.premia
obj instanceof Klasaczy 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 callbackubtn.addEventListener('click', () => l.zwieksz())
Object.getPrototypeOf(obj)prototyp obiektu (=== Klasa.prototype)sprawdzenie, co jest pod spodem

Zadania

Zadanie 1
łatwe

Produkt z ceną brutto

Napisz klasę Produkt z konstruktorem (nazwa, cenaNetto, vat = 23) i:

  • getterem cenaBrutto zwracają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
produkt.js
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: Chleb
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik
Zadanie 2
średnie

Konto 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 rzuca Error z komunikatem Przekroczono limit debetu,
  • getter saldo i getter wDebecie (true, gdy saldo ujemne),
  • każda operacja dopisuje do historia obiekt { typ: 'wplata' | 'wyplata', kwota, saldoPo },
  • metoda toJSON() zwraca { wlasciciel, limit, saldo, historia }, aby JSON.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
konto-limit.js
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 true
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik
Zadanie 3
średnie

Biblioteka książek z filtrowaniem

Zbuduj dwie klasy i interfejs:

  • Ksiazka(tytul, autor, rok) z polem wypozyczona = false i metodą przelacz() (zmienia stan na przeciwny),
  • Biblioteka trzyma tablicę książek (prywatnie), ma metody dodaj(ksiazka), szukaj(fraza) (po tytule lub autorze, bez rozróżniania wielkości liter), dostepne() i render(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
Biblioteka
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();
Podgląd
Zadanie 4
trudne

Hierarchia figur z walidacją

Zaprojektuj klasę bazową Figura i trzy klasy pochodne: Kolo(r), Prostokat(a, b), Trojkat(a, b, c). Wymagania:

  • Figura ma pole() i obwod(), które rzucają Error('Nie zaimplementowano') – klasy pochodne je nadpisują,
  • konstruktor Trojkat sprawdza 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 do sort,
  • getter nazwa zwraca nazwę klasy małymi literami (this.constructor.name.toLowerCase()),
  • toString() w Figura daje np. kolo: pole 12.57, obwód 12.57 (nazwa z gettera nazwa) – 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
figury.js
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)); // true
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik
Zadanie 5
trudne

Kół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 getterem koniec,
  • metoda zagraj(indeks) – ignoruje ruch, gdy pole zajęte lub gra skończona; po ruchu sprawdza zwycięzcę (8 linii) i remis; zwraca true, gdy ruch wykonano, a false, gdy go zignorowano,
  • reset(),
  • gettery plansza (kopia tablicy, żeby nikt nie zmieniał stanu z zewnątrz) i komunikat (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
Kółko i krzyżyk
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();
Podgląd

Test powtórkowy

Test powtórkowy

Klasy, akcesory, pola prywatne, static i dziedziczenie. Jedna poprawna odpowiedź.

0/0poprawnych
Sprawdź się

Który zapis poprawnie tworzy obiekt z klasy Samochod?

Sprawdź się

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);
Sprawdź się

Które zdanie o polu #stan w klasie jest prawdziwe?

Sprawdź się

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);
Sprawdź się

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');
Sprawdź się

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);
Sprawdź się

Co wypisze ten kod?

class Rodzic {
powitanie() { return 'Cześć'; }
}
class Dziecko extends Rodzic {
powitanie() { return super.powitanie() + '!'; }
}
console.log(new Dziecko().powitanie());
Sprawdź się

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);
Sprawdź się

Które stwierdzenie o klasach w JavaScript jest prawdziwe?

Sprawdź się

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);