NOVINKA - Online rekvalifikační kurz Python programátor. Oblíbená a studenty ověřená rekvalifikace - nyní i online.
Hledáme nové posily do ITnetwork týmu. Podívej se na volné pozice a přidej se do nejagilnější firmy na trhu - Více informací.

Lekce 4 - Referenční a hodnotové datové typy v JavaScriptu

V předchozím kvízu, Kvíz - Úvod a vlastnosti objektů v JavaScriptu, jsme si ověřili nabyté zkušenosti z předchozích lekcí.

V dnešním tutoriálu JavaScript OOP se podíváme na hodnotové a referenční datové typy. Začínáme pracovat s objekty. Objekty jsou referenčními datovými typy, které se v některých ohledech chovají jinak, než typy hodnotové (například čísla). Je důležité přesně vědět, co se uvnitř programu děje, jinak by nás v budoucnu mohlo leccos překvapit.

Hodnotové a referenční datové typy

Hodnotové datové typy jsou většinou jednoduché struktury, například jedno číslo. Většinou se chce, abychom s nimi pracovali co nejrychleji. V programu se jich vyskytuje velmi mnoho a zabírají málo místa. V anglické literatuře jsou často popisovány slovy light-weight. Příkladem jsou proměnné typu number nebo boolean.

Aplikace má operačním systémem přidělenou paměť v podobě takzvaného zásobníku (stack). Jedná se o velmi rychlou paměť s přímým přístupem, aplikace její velikost nemůže ovlivnit. Prostředky jsou přidělovány operačním systémem. Zjednodušeně můžeme říci, že tato malá a rychlá paměť je využívána k ukládání lokálních proměnných hodnotového typu. Hodnotové typy jsou v JavaScriptu někdy synonymem pro takzvané immutable objekty. Ale zatím se těmito věcmi nebudeme trápit :)

Proměnnou hodnotového typu si představme v paměti takto:

Proměnná hodnotového typu v paměti stack v JavaScriptu - Objektově orientované programování v JavaScriptu

Na obrázku je znázorněna paměť, kterou může naše aplikace využívat. V aplikaci jsme si vytvořili proměnnou a, které JavaScript přiřadil typ number. Její hodnota je 56 a uložila se nám přímo do zásobníku. Kód vypadá takto:

let a = 56;

Můžeme to chápat tak, že proměnná a má přidělenu část paměti v zásobníku (velikosti datového typu number, tedy 64 bitů), ve které je uložena hodnota 56.

Referenční datový typ

Vytvořme si novou jednoduchou třídu, která bude reprezentovat uživatele nějakého systému:

class Uzivatel {
    constructor(jmeno, vek) {
        this.jmeno = jmeno;
        this.vek = vek;
    }

    toString() {
        return `${this.jmeno} (${this.vek})`;
    }
}

Třída má dvě jednoduché vlastnosti, konstruktor a metodu toString(), abychom uživatele mohli jednoduše vypisovat. V tomto případě se metoda toString() používá pro automatické poskytnutí reprezentace instance třídy Uzivatel jako řetězce při jakémkoli jejím převodu na řetězec v rámci programu. V dalším skriptu v obsluha.js vytvoříme instanci této třídy:

let a = 56;
let jan = new Uzivatel("Jan Novák", 28);

Proměnná jan s uživatelem je nyní referenčního typu. K uložení instance jsme tentokrát použili let namísto const, protože hodnotu proměnné budeme za okamžik měnit. Na uložení proměnné to nemá žádný vliv. Podívejme se na novou situaci v paměti:

Referenční typ na haldě v JavaScriptu - Objektově orientované programování v JavaScriptu

Vidíme, že objekt (proměnná referenčního datového typu) se již neukládá do zásobníku, ale do paměti zvané halda. Je to z toho důvodu, že objekt je zpravidla složitější než hodnotový datový typ (většinou obsahuje hned několik dalších atributů) a také zabírá v paměti více místa. Zásobník i halda se nacházejí v paměti RAM.

Způsob uložení referenčních datových typů

Proměnné referenčního typu jsou v paměti uloženy vlastně nadvakrát, jednou v zásobníku a jednou v haldě. V zásobníku je uložena pouze takzvaná reference, tedy odkaz do haldy, kde se poté nalézá opravdový objekt.

Můžeme se ptát, proč je to takto udělané. Důvodů je hned několik, pojďme si některé vyjmenovat:

  1. Místo ve stacku je omezené.
  2. Když budeme chtít použít objekt vícekrát (například ho předat jako parametr do několika metod), nemusíme ho v programu předávat jako kopii. Předáme pouze malý hodnotový typ s referencí na objekt, místo toho, abychom obecně paměťově náročný objekt kopírovali. Toto si vzápětí ukážeme.
  3. Pomocí referencí můžeme jednoduše vytvářet struktury s dynamickou velikostí. Například struktury podobné poli, do kterých můžeme za běhu vkládat nové prvky. Ty jsou na sebe navzájem odkazovány referencemi, jako řetěz objektů.

Založme si dvě číselné proměnné a dvě instance třídy Uzivatel:

let a = 56;
let b = 28;
let jan = new Uzivatel("Jan Novák", 28);
let josef = new Uzivatel("Josef Nový", 32);

Situace v paměti bude následující:

Reference na haldu v JavaScriptu - Objektově orientované programování v JavaScriptu

Předávání hodnot a referencí

Nyní zkusme přiřadit do proměnné a proměnnou b. Stejně tak přiřadíme i proměnnou josef do proměnné jan. Hodnotový typ se v zásobníku jen zkopíruje. U objektu se zkopíruje pouze reference (což je vlastně také hodnotový typ). V kódu vykonáme tedy toto:

let a = 56;
let b = 28;
let jan = new Uzivatel("Jan Novák", 28);
let josef = new Uzivatel("Josef Nový", 32);
// Přiřazení
a = b;
jan = josef;

V paměti bude celá situace vypadat následovně:

Ukládání referencí v JavaScriptu - Objektově orientované programování v JavaScriptu

Přesvědčme se o tom, že to tak opravdu je :) Nejprve si necháme všechny čtyři proměnné vypsat před a po změně. Protože budeme výpis proměnných volat vícekrát, napíšeme si pro něj v souboru obsluha.js jednoduchou funkci. Tentokrát budeme proměnné vypisovat do konzole do přehledné tabulky, kde bude na každém řádku jedna proměnná a ve sloupcích její hodnoty. Upravme tedy kód na následující:

function vypisPromenne() {
    console.table({
            "a": a,
            "b": b,
            "jan": jan,
            "josef": josef
        });
}

let a = 56;
let b = 28;
let jan = new Uzivatel("Jan Novák", 28);
let josef = new Uzivatel("Josef Nový", 32);

vypisPromenne();

// Přiřazení
a = b;
jan = josef;

vypisPromenne();

Po spuštění v prohlížeči si otevřeme vývojářskou konzoli pomocí F12, poté klikneme na záložku Console, kde bude podobná tabulka:

Debug výpis v podobě tabulky do konzole - Objektově orientované programování v JavaScriptu

Změna vlastnosti objektu

Na výstupu programu zatím rozdíl mezi hodnotovým a referenčním typem nepoznáme. Nicméně víme, že zatímco v a a b jsou opravdu dvě různá čísla se stejnou hodnotou, v jan a josef je ten samý objekt. Pojďme změnit jméno uživatele josef. Dle našich předpokladů se změna projeví i v proměnné jan. K obsluze připíšeme:

josef.jmeno = "John Doe";

vypisPromenne();

Změnili jsme objekt v proměnné josef a znovu vypíšeme jan a josef:

Reference v JavaScriptu - Objektově orientované programování v JavaScriptu

Spolu se změnou proměnné josef se změní i proměnná jan, protože ukazují na ten samý objekt. Opravdovou kopii objektu nejjednodušší cestou získáme tak, že objekt znovu vytvoříme pomocí konstruktoru a dáme do něj stejná data. Připomeňme si situaci v paměti ještě jednou a zaměřme se na Jana Nováka:

Garbage Collector v JavaScriptu - Objektově orientované programování v JavaScriptu

Co se s ním stane? "Sežere" ho takzvaný Garbage collector:

Garbage Collector - Objektově orientované programování v JavaScriptu

Garbage collector a dynamická správa paměti

Paměť můžeme v programech alokovat staticky. To znamená, že ve zdrojovém kódu předem určíme, kolik paměti budeme používat. Doposud jsme to tak vlastně dělali a neměli jsme s tím problém. Do zdrojového kódu jsme napsali potřebné proměnné. Brzy se ale budeme setkávat s aplikacemi, kdy nebudeme před spuštěním přesně vědět, kolik paměti budeme potřebovat. V tomto případě hovoříme o dynamické správě paměti.

V minulosti, hlavně v dobách jazyků C a Pascal a C++, se k tomuto účelu používaly takzvané pointery, neboli přímé ukazatele do paměti. Vesměs to fungovalo tak, že jsme si řekli operačnímu systému o kus paměti o určité velikosti. On ji pro nás vyhradil a dal nám její adresu. Na toto místo v paměti jsme měli pointer, přes který jsme s pamětí pracovali. Problém byl, že nikdo nehlídal, co do paměti dáváme (ukazatel směřoval na začátek vyhrazeného prostoru). Když jsme tam dali něco většího, zkrátka se to stejně uložilo a přepsala se data za naším prostorem, která patřila třeba jinému programu nebo operačnímu systému (v tom případě by naši aplikaci OS asi zabil - zastavil). Často jsme si však my v paměti přepsali nějaká další data našeho programu a program se začal chovat chaoticky. Představme si, že si uložíme uživatele do pole a v tu chvíli se nám najednou změní barva uživatelského prostředí, tedy něco, co s tím vůbec nesouvisí. Hodiny strávíme tím, že kontrolujeme kód pro změnu barvy, poté zjistíte, že je chyba v založení uživatele, kdy dojde k přetečení paměti a přepsání hodnot barvy.

Jednou někdo pravil: "Lidský mozek se nedokáže starat ani o správu paměti vlastní, natož aby řešil memory management programu." Měl samozřejmě pravdu, až na malou skupinu géniů lidi přestalo bavit řešit neustálé a nesmyslné chyby. Za cenu mírného snížení výkonu vznikly řízené (managed) jazyky s takzvaným Garbage collectorem. jedním z nich je i JavaScript.

Garbage collector - Objektově orientované programování v JavaScriptu

Garbage collector je vlastně program, který běží paralelně s naší aplikací, v samostatném vlákně. Občas se spustí a podívá se, na které objekty již v paměti nevedou žádné reference. Ty potom odstraní. Ztráta výkonu je minimální a značně to sníží procento sebevražd programátorů, ladících po večerech rozbité pointery. Protože je jazyk řízený a nepracujeme s přímými pointery, není vůbec možné paměť nějak narušit, nechat ji přetéct a podobně, interpret se o paměť automaticky stará.

Hodnota null a undefined

Poslední věc, o které se dnes zmíníme, jsou hodnoty null a undefined.

Hodnota null

Klíčové slovo null označuje, že reference neukazuje na žádná data. Když nastavíme proměnnou jan na null, zrušíme pouze tu jednu referenci. Pokud na náš objekt existuje ještě nějaká reference, bude i nadále existovat. Pokud ne, bude uvolněn Garbage collectorem. Změňme ještě poslední řádky našeho programu na:

josef.jmeno = "John Doe";
jan = null;

vypisPromenne();

Dostaneme:

Hodnota null v JavaScriptu - Objektově orientované programování v JavaScriptu

Vidíme, že objekt stále existuje a ukazuje na něj proměnná josef, v proměnné jan již není reference.

Hodnota undefined

Nyní se dostáváme k undefined. Když si vytvoříme nějakou proměnnou, které ovšem nenastavíme hodnotu, bude v ní právě undefined. Zkusme si to:

let promenna;
console.log(promenna);

Tato hodnota je také například na indexech polí, na kterých zatím není žádná hodnota. Označuje tedy, že proměnná existuje, ale nic neobsahuje. To samé by se stalo, pokud bychom vytvořili nějakou funkci s parametrem a parametr při volání nezadali:

function zkouska(parametr) {
    console.log(parametr);
}

zkouska();

null na rozdíl od undefined označuje, že proměnná již byla uložena a byla nastavena právě na prázdnou referenci.

V příští lekci, Tvorba OOP diáře v JavaScriptu, si naprogramujeme elektronický diář :)


 

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 194x (2.13 kB)
Aplikace je včetně zdrojových kódů v jazyce JavaScript

 

Jak se ti líbí článek?
Před uložením hodnocení, popiš prosím autorovi, co je špatněZnaků 0 z 50-500
Předchozí článek
Kvíz - Úvod a vlastnosti objektů v JavaScriptu
Všechny články v sekci
Objektově orientované programování v JavaScriptu
Přeskočit článek
(nedoporučujeme)
Tvorba OOP diáře v JavaScriptu
Článek pro vás napsal Šimon Raichl
Avatar
Uživatelské hodnocení:
286 hlasů
Autor se věnuje hlavně tvorbě všemožných věcí v JS
Aktivity