9. března 2025

Referenční typ

Hlubší vlastnost jazyka

Tento článek se zabývá pokročilým tématem, abychom lépe porozuměli určitým okrajovým případům.

Toto téma není důležité. Mnoho zkušených vývojářů žije šťastně i bez jeho znalosti. Článek si přečtěte, pokud chcete vědět, jak fungují věci „pod kapotou“.

Dynamicky vyhodnocované volání metody může ztratit this.

Například:

let uživatel = {
  jméno: "Jan",
  ahoj() { alert(this.jméno); },
  nashle() { alert("Nashle"); }
};

uživatel.ahoj(); // funguje

// nyní podle jména zavolejme uživatel.ahoj nebo uživatel.nashle
(uživatel.jméno == "Jan" ? uživatel.ahoj : uživatel.nashle)(); // Chyba!

Na posledním řádku je podmíněný operátor, který vybere buď uživatel.ahoj, nebo uživatel.nashle. V tomto případě je výsledek uživatel.ahoj.

Pak je tato metoda okamžitě volána pomocí závorek (). Ale nefunguje to správně!

Jak vidíte, výsledkem volání je chyba, protože hodnota "this" uvnitř volání se stala undefined.

Tohle funguje (objekt tečka metoda):

uživatel.ahoj();

Tohle ne (vyhodnocená metoda):

(uživatel.jméno == "Jan" ? uživatel.ahoj : uživatel.nashle)(); // Chyba!

Proč? Chceme-li porozumět, proč se to děje, podívejme se na zoubek tomu, jak funguje volání obj.metoda().

Vysvětlení referenčního typu

Když se podíváme pozorněji, můžeme si v příkazu obj.metoda() všimnout dvou operací:

  1. Nejprve tečka '.' získá vlastnost obj.metoda.
  2. Pak ji závorky () spustí.

Jak se tedy informace o this předá z první části do druhé?

Umístíme-li tyto operace na samostatné řádky, pak bude this zcela jistě ztraceno:

let uživatel = {
  jméno: "Jan",
  ahoj() { alert(this.jméno); }
}

// rozdělíme získání a volání metody na dva řádky
let ahoj = uživatel.ahoj;
ahoj(); // Chyba, protože this je undefined

Zde ahoj = uživatel.ahoj vloží funkci do proměnné a ta je pak na posledním řádku zcela samostatná, takže tam není žádné this.

Aby volání uživatel.ahoj() fungovalo, JavaScript používá trik – tečka '.' nevrací funkci, ale hodnotu speciálního referenčního typu.

Referenční typ je „specifikační typ“. Nemůžeme jej explicitně používat, ale je používán vnitřně jazykem.

Hodnotou referenčního typu je tříhodnotová kombinace (base, name, strict), kde:

  • base (základ) je objekt.
  • name (název) je název vlastnosti.
  • strict (striktní) je true, pokud je použito use strict.

Výsledkem přístupu k vlastnosti uživatel.ahoj není funkce, ale hodnota referenčního typu. Pro uživatel.ahoj ve striktním režimu to je:

// hodnota referenčního typu
(uživatel, "ahoj", true)

Když se na referenčním typu zavolají závorky (), obdrží úplnou informaci o objektu a jeho metodě a mohou tedy nastavit správné this (v tomto případě uživatel).

Referenční typ je speciální „zprostředkovatelský“ interní typ, jehož účelem je předat informaci z tečky . volajícím závorkám ().

Jakákoli jiná operace, např. přiřazení ahoj = uživatel.ahoj, celý referenční typ zahodí, vezme hodnotu uživatel.ahoj (funkci) a předá ji dál. Jakákoli další operace tedy „ztratí“ this.

Výsledkem tedy je, že hodnota this se předá správně jen tehdy, je-li funkce volána přímo pomocí syntaxe tečky obj.metoda() nebo hranatých závorek obj['metoda']() (obojí zde provádí totéž). Existují různé způsoby, jak tento problém vyřešit, např. funkce.bind().

Shrnutí

Referenční typ je interní jazykový typ.

Načtení vlastnosti, např. pomocí tečky . v obj.metoda(), nevrací přesně hodnotu vlastnosti, ale speciální hodnotu „referenčního typu“, v níž je uložena jak hodnota vlastnosti, tak objekt, z něhož byla převzata.

To je proto, aby následné volání metody () mohlo získat objekt a nastavit jej jako this.

Při všech ostatních operacích se z referenčního typu automaticky stává hodnota vlastnosti (v našem případě funkce).

Celá tato mechanika je před našima očima ukryta. Záleží na ní jen v krajních případech, například když je metoda získána z objektu dynamicky použitím výrazu.

Úlohy

důležitost: 2

Jaký je výsledek tohoto kódu?

let uživatel = {
  jméno: "Jan",
  jdi: function() { alert(this.jméno) }
}

(uživatel.jdi)()

P.S. Je tady chyták :)

Chyba!

Zkuste si to:

let uživatel = {
  jméno: "Jan",
  jdi: function() { alert(this.jméno) }
}

(uživatel.jdi)() // chyba!

Ve většině prohlížečů nám chybová zpráva nedává mnoho informací o tom, co bylo špatně.

Chyba se objevila proto, že za uživatel = {...} chybí středník.

JavaScript automaticky nevloží středník před závorku (uživatel.jdi)(), takže přečte kód jako:

let uživatel = { jdi:... }(uživatel.jdi)()

Pak také vidíme, že takový spojený výraz je syntakticky voláním objektu { jdi: ... } jako funkce s argumentem (uživatel.jdi). A to se také odehrává na stejném řádku jako let uživatel, takže objekt uživatel ještě ani nebyl definován, proto nastane chyba.

Jestliže vložíme středník, bude vše v pořádku:

let uživatel = {
  jméno: "Jan",
  jdi: function() { alert(this.jméno) }
};

(uživatel.jdi)() // Jan

Prosíme všimněte si, že závorky okolo (uživatel.jdi) tady nic nedělají. Obvykle nastavují pořadí operací, ale tady jako první zafunguje tečka . tak jako tak, takže závorky nemají žádný efekt. Vadí jenom chybějící středník.

důležitost: 3

V následujícím kódu jsme měli v úmyslu volat metodu obj.jdi() čtyřikrát za sebou.

Avšak volání (1) a (2) fungují jinak než (3) a (4). Proč?

let obj, metoda;

obj = {
  jdi: function() { alert(this); }
};

obj.jdi();               // (1) [object Object]

(obj.jdi)();             // (2) [object Object]

(metoda = obj.jdi)();    // (3) undefined

(obj.jdi || obj.stůj)(); // (4) undefined

Zde je vysvětlení.

  1. Toto je běžné volání metody objektu.

  2. Totéž, závorky tady nezmění pořadí operací, tečka je i tak první.

  3. Zde máme složitější volání (výraz)(). Toto volání funguje tak, jako by bylo rozděleno na dva řádky:

    f = obj.jdi; // vypočítáme výraz
    f();         // zavoláme to, co máme

    Zde se f() spustí jako funkce bez this.

  4. Podobně jako (3), nalevo od závorek () máme výraz.

Abychom vysvětlili chování (3) a (4), musíme si vzpomenout, že operátory přístupu k vlastnostem (tečka nebo hranaté závorky) vracejí hodnotu referenčního typu.

Jakákoli operace na ní kromě volání metody (např. přiřazení =, nebo ||) ji změní na obyčejnou hodnotu, která neobsahuje informaci umožňující nastavit this.

Mapa tutoriálu

Komentáře

přečtěte si před komentováním…
  • Máte-li návrhy na zlepšení, vytvořte prosím issue na GitHubu nebo pull request místo komentáře.
  • Pokud v článku něčemu nerozumíte, napište prosím, čemu přesně a na kterém místě.
  • Pro vložení několika slov kódu použijte značku <code>, pro několik řádků je obalte značkou <pre>, pro více než 10 řádků vložte odkaz na pískoviště (plnkr, jsbin, codepen…)