NOVINKA: Staň se datovým analytikem od 0 Kč a získej jistotu práce, lepší plat a nové kariérní možnosti. Více informací:

Lekce 5 - Tvorba OOP diáře v JavaScriptu

V minulé lekci, Referenční a hodnotové datové typy v JavaScriptu, jsme si vysvětlili rozdíly mezi hodnotovými a referenčními datovými typy. Již víme, že když uložíme instanci třídy do nějaké proměnné, je v ní ve skutečnosti uložena reference (odkaz) na tuto instanci. Můžeme tak používat jednu instanci z několika proměnných nebo ji jednoduše předávat, aniž by se zkopírovala.

V dnešním tutoriálu objektově orientovaného programování v JavaScriptu začneme programovat elektronický diář. Do diáře budeme schopni zadávat úkoly, mazat je, vypisovat a označovat jako splněné. Naším cílem bude tento výsledek:

Diář
index.html

Opakování OOP

Při tvorbě diáře využijeme také své dosavadní znalosti objektově orientovaného programování (OOP). Připomeňme si v rychlosti, co o OOP již víme. Základní jednotkou je objekt tvořený vlastnostmi a metodami. Abychom mohli vytvořit objekt, musíme si nejprve vytvořit třídu. Třída je vzor, podle kterého se objekty vytváří a který definuje, jaké vlastnosti a metody dané objekty mají mít.

Například v námi vytvářeném elektronickém diáři budeme mít třídu Zaznam definující vlastnosti jako název, datum a stav splnění. Tato třída bude šablonou, podle které se vytvoří každý záznam (objekt) s požadovanými vlastnostmi.

Základy jsme si osvěžili a můžeme se podívat na něco nového :)

Základní pilíře OOP

OOP stojí na třech základních pilířích:

  • zapouzdření,
  • dědičnost,
  • polymorfismus.

Tyto pilíře si v kurzu postupně představíme. Dnes použijeme první z nich.

Příprava projektu

Nejdříve se ale zamyslíme nad tím, co vše budeme potřebovat. Co se týče JavaScriptu, vytvoříme si složku js/ a v ní tři skripty:

  • Diar.js,
  • Zaznam.js,
  • obsluha.js.

Dále si v kořenové složce vytvoříme jednoduchou HTML stránku index.html s:

  • formulářem na přidání úkolu (záznamu) nahoře,
  • výpisem úkolů (záznamů) dole.

Soubor index.html bude vypadat takto:

<!DOCTYPE html>
<html lang="cs-cz">
<head>
    <meta charset="UTF-8">
    <title>Diář</title>
</head>
<body>
    <h1>Diář</h1>
    <div>
        <input type="text" id="nazev" placeholder="Vyplňte název úkolu"><br>
        <input type="date" id="datum" placeholder="Vyplňte datum"><br>
        <button id="potvrdit">Uložit úkol</button>
    </div>
    <div id="seznam-ukolu">

    </div>
    <script src="js/Diar.js"></script>
    <script src="js/Zaznam.js"></script>
    <script src="js/obsluha.js"></script>
</body>
</html>

Do elementu <div> jsme vložili dva elementy <input> typu text a date na název úkolu a jeho datum. Do stejného elementu <div> jsme přidali ještě i tlačítko na potvrzení. Níže jsme pak vložili druhý <div> pro seznam úkolů a rovnou i naodkazovali naše skripty.

Stránka vypadá v prohlížeči následovně:

Tvoje stránka
localhost

Vzhled nyní nebudeme příliš řešit.

Záznam

Začneme se souborem Zaznam.js. Jak asi tušíme, soubor bude obsahovat třídu Zaznam reprezentující jeden záznam v našem diáři. Dáme jí různé vlastnosti, které při vytvoření a po vytvoření záznamu budeme moci měnit. Zatím to budou:

  • nazev – název,
  • datum – datum,
  • splneno – informace, zda byl úkol splněn.

První dvě vlastnosti nastavíme konstruktorem (inicializujeme nový záznam s daným názvem a datem). Co se týká splnění úkolu, tak budeme předpokládat, že je po vytvoření vždy nesplněný:

class Zaznam {
    nazev;
    datum;
    splneno = false;

    constructor(nazev, datum) {
        this.nazev = nazev;
        this.datum = datum;
    }
}

Připomeňme si, že klíčovým slovem this zde odkazujeme na aktuálně vytvářenou instanci třídy Zaznam.

Diář

Přesuňme se nyní k samotnému diáři. V souboru Diar.js si vytvoříme třídu Diar.

Základní atributy

Třídě zatím přidáme dva třídní atributy:

  • zaznamy – Záznamy diáře uložené v poli.
  • jazyk – Jazyk výpisu data záznamů, který se nám bude hodit v budoucnu, jelikož různé jazyky mají různé formáty. Například české datum vypadá jinak než anglické.

Třída Diar bude nyní vypadat následovně:

class Diar {
    zaznamy = [];
    jazyk;
}

Z minulých lekcí víme, že takto definované vlastnosti pomocí třídních atributů můžeme při vytváření a po vytvoření instance libovolně měnit, a to i zvenčí. To však v mnoha případech není žádoucí. Proč si nyní vysvětlíme spolu s pojmem zapouzdření.

Zapouzdření

Zapouzdření umožňuje skrýt některé metody a vlastnosti tak, aby zůstaly použitelné jen pro třídu zevnitř. Objekt si můžeme představit jako černou skřínku (anglicky blackbox), která má určité rozhraní (interface), přes které jí předáváme instrukce/data a ona je zpracovává.

Nevíme, jak to uvnitř funguje, ale víme, jak se navenek chová a používá. Nemůžeme tedy způsobit nějakou chybu, protože využíváme a vidíme jen to, co tvůrce třídy zpřístupnil.

Příkladem může být třída Clovek, která bude mít vlastnost datumNarozeni a na jeho základě další vlastnosti jako plnolety a vek. Kdyby někdo objektu zvenčí změnil datumNarozeni, přestaly by platit vlastnosti plnolety a vek. Říkáme, že vnitřní stav objektu by byl nekonzistentní. Toto se nám ve strukturovaném programování může klidně stát. V OOP však objekt zapouzdříme. Třídní atribut odpovídající vlastnosti datumNarozeni označíme jako privátní a tím pádem bude jasné, že nechceme, aby nám danou vlastnost někdo jen tak měnil. Naopak ven vystavíme metodu zmenDatumNarozeni(), která dosadí nové datum narození do vlastnosti datumNarozeni a zároveň provede potřebný přepočet věku a přehodnocení plnoletosti. Použití objektu je bezpečné a aplikace stabilní.

Zapouzdření tedy tlačí programátory používat objekt jen tím správným způsobem. Rozhraní (interface) třídy rozdělí na veřejně přístupné a vnitřní (privátní) strukturu.

Zapouzdření atributů

Vraťme se k naší třídě Diar. Nyní jsou všechny atributy třídy veřejně přístupné. My však nechceme a ani nepotřebujeme, aby se daly odpovídající vlastnosti zvenčí modifikovat a hrozilo tak, že se vnitřní stav objektu stane nekonzistentní. Chceme je tedy označit jako privátní.

V takovém případě musíme atribut pojmenovat se znakem mřížky # na začátku, například #zaznamy. Vlastnost odpovídající danému atributu je poté viditelná jen uvnitř třídy a zvenčí se JavaScript tváří, že vůbec neexistuje. Pokud se pokusíme přistoupit k privátní vlastnosti zvenčí, bude vyvolána chyba.

Při návrhu třídy všechny atributy i metody definujeme jako privátní s mřížkou # na začátku. Až v případě, že je něco opravdu potřeba vystavit, mřížku nepoužijeme.

Přejmenujme tedy naše dosavadní atributy tak, aby byly privátní:

class Diar {
    #zaznamy = [];
    #jazyk;
}

Atributy třídy Zaznam však ponecháme veřejné. Budeme k nim totiž chtít přistupovat ve třídě Diar, tedy zvenčí.

Inicializace jazyka

Dále si do třídy Diar přidáme konstruktor, ve kterém inicializujeme vlastnost #jazyk na hodnotu z parametru:

class Diar {
    #zaznamy = [];
    #jazyk;

    constructor(jazyk = "cs-CZ") {
        this.#jazyk = jazyk;
    }
}

Protože budeme chtít většinou české prostředí, definujeme parametru konstruktoru výchozí hodnotu "cs-CZ". Tato hodnota se použije, pokud parametr při vytváření instance nezadáme.

Vybírání elementů na stránce

Ve třídě budeme potřebovat pracovat s DOM elementy na stránce. Existuje pravidlo, které říká, že je dobrým principem oddělení práce s logikou od práce s uživatelským rozhraním. Tohoto základního programátorského pravidla využívá například architektura MVC.

V javascriptových frameworcích, ke kterým se dostaneme po tomto kurzu, je architektura aplikace postavená tak, aby se nemíchal kód vybírající elementy na stránce s dalším kódem aplikace. Bylo by to totiž velmi nepřehledné.

My si v základním OOP kurzu nebudeme vytvářet žádnou komplikovanou architekturu, ale budeme se snažit umístit vybírání elementů ze stránky na jedno jediné místo ve třídě.

Do třídy si tedy přidáme několik dalších privátních atributů pro elementy ze stránky, které budeme ve třídě dále potřebovat:

  • #nazevInput – element <input> s názvem nově přidávaného záznamu,
  • #datumInput – element <input> s datem nově přidávaného záznamu,
  • #potvrditButton – ukládací tlačítko,
  • #vypisElement – element pro výpis záznamů uložených v diáři.

Nové atributy umístíme pod ty existující a rovnou je napojíme na odpovídající DOM prvky ze stránky:

class Diar {
    #zaznamy = [];
    #jazyk;
    #nazevInput = document.getElementById("nazev");
    #datumInput = document.getElementById("datum");
    #potvrditButton = document.getElementById("potvrdit");
    #vypisElement = document.getElementById("seznam-ukolu");
    ...
}

Elementy vybíráme pomocí metody getElementById().

Nikam jinam ve třídě nebudeme dále vkládat žádný další výběr elementů, protože by to bylo velmi nepřehledné.

Metody

Přejděme k metodám diáře.

Metoda #nastavUdalosti()

Aby náš konstruktor nebyl příliš dlouhý, vyčleníme nastavení obslužných událostí elementům na stránce do oddělené privátní metody #nastavUdalosti(). V našem případě jde jen o obsluhu kliknutí na tlačítko potvrzení.

Zatím si přidejme chybnou naivní implementaci metody pro obsluhu tlačítka, která nebude fungovat. Proč si vysvětlíme za okamžik:

#nastavUdalosti() {
    this.#potvrditButton.onclick = function() { // tento kód nebude fungovat
        const zaznam = new Zaznam(this.#nazevInput.value, this.#datumInput.value);
        this.#zaznamy.push(zaznam);
        this.vypisZaznamy();
    };
}

Metoda na událost onclick tlačítka this.#potvrditButton naváže obslužnou funkci. Zde je ještě vše v pořádku. Uvnitř se vezmou hodnoty z inputů a na jejich základě se vytvoří nová instance záznamu. Tuto novou instanci vložíme do pole záznamů #zaznamy. Všechny záznamy poté vypíšeme na stránku pomocí metody vypisZaznamy(), kterou si implementujeme za chvíli.

Co je tedy špatně? Pokud jste dávali v Základních konstrukcích JavaScriptu pozor, víte, že:

Při použití function pro obsluhu událostí elementů se mění kontext a klíčové slovo this následně ukazuje na element, který událost způsobil. this tedy přestane obsahovat instanci naší třídy. Toto chování je chyba v návrhu jazyka JavaScript a zabraňuje nám pracovat s instančními proměnnými a metodami v obsluze událostí.

Arrow functions

Způsobů, jak tento problém obejít, je hned několik. My si zmíníme to nejjednodušší řešení. K obsluze události použijeme takzvanou arrow function, což je "zkrácený" zápis funkce. Název vychází ze znaku šipky (anglicky arrow), kterým se tyto funkce zapisují. Arrow functions byly do JavaScriptu přidány až později, a proto chybou změny kontextu již netrpí. Přesněji ani žádný svůj kontext nemají a klíčové slovo this v nich obsahuje to, co v něm bylo předtím, beze změny.

Pokud tedy vytvoříme arrow function uvnitř třídy, klíčové slovo this bude v této funkci obsahovat stejnou hodnotu jako this dané třídy, to znamená aktuální instanci dané třídy. Vše tak bude fungovat dle očekávání.

Arrow function do proměnné uložíme následujícím způsobem:

nazevFunkce = () => {
    // tělo funkce
}

Pokud bychom chtěli funkci poslat i parametry, můžeme je psát do závorek, jak jsme zvyklí:

nazevFunkce = (parametr) => {
    // tělo funkce
}

Pokud bychom chtěli poslat pouze jeden, můžeme závorky dokonce i vynechat:

nazevFunkce = parametr => {
    // tělo funkce
}

Nyní tedy metodu #nastavUdalosti() opravíme, aby v obslužné funkci fungovalo klíčové slovo this. To totiž používáme k přístupu k vlastnostem a metodám naší třídy:

#nastavUdalosti() {
    this.#potvrditButton.onclick = () => {
        const zaznam = new Zaznam(this.#nazevInput.value, this.#datumInput.value);
        this.#zaznamy.push(zaznam);
        this.vypisZaznamy();
    };
}

Metodu #nastavUdalosti() zavoláme na konci konstruktoru:

constructor(jazyk = "cs-CZ") {
    this.#jazyk = jazyk;

    this.#nastavUdalosti();
}

Metoda vypisZaznamy()

V metodě vypisZaznamy() pro výpis záznamů nás asi nic nepřekvapí. Funguje velmi podobně jako výpis zaměstnanců, který jsme vytvářeli v předešlých lekcích:

vypisZaznamy() {
    this.#vypisElement.innerHTML = "";
    for (const zaznam of this.#zaznamy) {
        this.#vypisElement.innerHTML += `<h3>${zaznam.nazev}</h3>kdy: ${zaznam.datum}<br>splněno: ${zaznam.splneno}`;
    }
}

Metoda maže veškerý obsah z našeho elementu pro výpis a vypisuje do něj postupně záznamy pomocí cyklu. Za zmínku ještě stojí, že jsme metodu ponechali veřejnou. Budeme ji totiž volat i zvenčí.

Výpis záznamů

Nakonec už jen musíme v souboru obsluha.js vytvořit instanci třídy Diar a vypsat uložené záznamy pomocí metody vypisZaznamy():

const diar = new Diar();
diar.vypisZaznamy();

Diář zatím ihned po svém vytvoření neobsahuje žádné záznamy. Dále v kurzu je ale budeme načítat z lokálního úložiště.

Pokud nyní naši aplikaci spustíme v prohlížeči, bude vypadat takto:

Tvoje stránka
localhost

V následujícím cvičení, Řešené úlohy k 4.-5. lekci OOP v JavaScriptu, si procvičíme nabyté zkušenosti z předchozích lekcí.


 

Měl jsi s čímkoli problém? Stáhni si vzorovou aplikaci níže a porovnej ji se svým projektem, chybu tak snadno najdeš.

Stáhnout

Stažením následujícího souboru souhlasíš s licenčními podmínkami

Staženo 805x (2.69 kB)
Aplikace je včetně zdrojových kódů v jazyce JavaScript

 

Předchozí článek
Referenční a hodnotové datové typy v JavaScriptu
Všechny články v sekci
Objektově orientované programování v JavaScriptu
Přeskočit článek
(nedoporučujeme)
Řešené úlohy k 4.-5. lekci OOP v JavaScriptu
Článek pro vás napsal Šimon Raichl
Avatar
Uživatelské hodnocení:
292 hlasů
Autor se věnuje především vývoji v JavaScriptu
Aktivity