Posledních několik let nám ukázalo, že vývoj webových stránek se mění. S tím, jak do prohlížečů přibývalo mnoho funkcí a rozhraní API, jsme je museli používat správným způsobem. Jazykem, kterému vděčíme za tuto poctu, byl JavaScript.
Zpočátku nebyli vývojáři přesvědčeni o tom, jak je navržen, a při používání tohoto skriptu měli většinou negativní dojmy. Postupem času se ukázalo, že tento jazyk má velký potenciál, a následné standardy ECMAScriptu některé mechaniky zlidšťují a jednoduše zlepšují. V tomto článku se na některé z nich podíváme.
Typy hodnot v JS
Známá pravda o JavaScript je, že všechno je zde objekt. Opravdu všechno: pole, funkce, řetězce, čísla a dokonce i logické symboly. Všechny typy hodnot jsou reprezentovány objekty a mají své vlastní metody a pole. Můžeme je však rozdělit do dvou kategorií: primitiva a strukturované objekty. Hodnoty první kategorie jsou neměnné, což znamená, že můžeme některé proměnné znovu přiřadit novou hodnotu, ale nemůžeme měnit samotnou stávající hodnotu. Druhá představuje hodnoty, které lze měnit, takže je třeba je interpretovat jako kolekce vlastností, které můžeme nahradit nebo jen zavolat metody, které jsou k tomu určeny.
Rozsah deklarovaných proměnných
Než se dostaneme hlouběji, vysvětleme si, co znamená rozsah. Můžeme říci, že obor je jediná oblast, kde můžeme používat deklarované proměnné. Před standardem ES6 jsme mohli proměnné deklarovat příkazem var a přidělit jim globální nebo lokální obor. První z nich je oblast, která umožňuje nás pro přístup k některým proměnným v libovolném místě aplikace, druhý je určen pouze pro určitou oblast - především funkci.
Od vydání standardu ES2015, JavaScript má tři způsoby deklarace proměnných, které se liší klíčovým slovem. První z nich je popsán dříve: proměnné deklarované klíčovým slovem var jsou rozprostřeny do aktuálního těla funkce. Standard ES6 nám umožnil deklarovat proměnné lidštějšími způsoby - na rozdíl od příkazů var jsou proměnné deklarované příkazy const a let skórovány pouze do bloku. JS však zachází s příkazem const zcela neobvykle, pokud jej ve srovnání s jinými příkazy programovací jazyky - místo persistované hodnoty uchovává persistovaný odkaz na hodnotu. Stručně řečeno, můžeme měnit vlastnosti objektu deklarovaného příkazem const, ale nemůžeme přepsat odkaz na tuto proměnnou. Někteří tvrdí, že alternativou příkazu var v ES6 je vlastně příkaz let. Ne, není, a příkaz var není a pravděpodobně ani nikdy nebude stažen. Dobrou praxí je vyhýbat se používání příkazů var, protože nám většinou způsobují další potíže. Naopak příkazy const musíme zneužívat, dokud nemusíme upravit jeho referenci - pak bychom měli použít let.
Příklad neočekávaného chování oboru
Začněme následujícími informacemi kód:
(() => {
for (var i = 0; i {
console.log(`Hodnota "i": ${i}`);
}, 1000);
}
})();
Když se na to podíváme, vypadá to, že cyklus for iteruje hodnotu i a po jedné sekundě zaznamená hodnoty iterátoru: 1, 2, 3, 4, 5. Ale není tomu tak. Jak jsme se zmínili výše, příkaz var je o tom, že se hodnota proměnné uchovává po celé tělo funkce; to znamená, že ve druhé, třetí atd. iteraci se hodnota proměnné i nahradí další hodnotou. Nakonec smyčka skončí a tiky timeoutu nám ukazují následující věc: 5, 5, 5, 5, 5, 5. Nejlepší způsob, jak udržet aktuální hodnotu iterátoru, je použít místo toho příkaz let:
(() => {
for (let i = 0; i {
console.log(`Hodnota "i": ${i}`);
}, 1000);
}
})();
Ve výše uvedeném příkladu udržujeme rozsah hodnoty i v aktuálním bloku iterace, je to jediná oblast, kde můžeme tuto proměnnou použít, a nic ji nemůže přepsat zvenčí. Výsledek je v tomto případě podle očekávání: 1 2 3 4 5. Podívejme se, jak tuto situaci řešit pomocí příkazu var:
(() => {
for (var i = 0; i {
setTimeout(() => {
console.log(`Hodnota "j": ${j}`);
}, 1000);
})(i);
}
})();
Protože příkaz var je o udržování hodnoty uvnitř bloku funkcí, musíme zavolat definovanou funkci, která přijme argument - hodnotu aktuálního stavu iterátoru - a pak teprve něco udělat. Nic mimo deklarovanou funkci hodnotu j nepřepíše.
Příklady nesprávných očekávání hodnot objektů
Nejčastěji jsem si všiml, že se dopouštím ignorování síly struktur a změny jejich vlastností, které se mění i v jiných částech kódu. Podívejte se na to zběžně:
const DEFAULT_VALUE = {
favoriteBand: 'The Weeknd'
};
const currentValue = DEFAULT_VALUE;
const bandInput = document.querySelector('#favorite-band');
const restoreDefaultButton = document.querySelector('#restore-button');
bandInput.addEventListener('input', () => {
currentValue.favoriteBand = bandInput.value;
}, false);
restoreDefaultButton.addEventListener('click', () => {
currentValue = DEFAULT_VALUE;
}, false);
Od začátku: předpokládejme, že máme model s výchozími vlastnostmi uložený jako objekt. Chceme mít tlačítko, které obnoví jeho vstupní hodnoty na výchozí. Po vyplnění vstupů nějakými hodnotami model aktualizujeme. Po chvíli si myslíme, že výchozí výběr byl prostě lepší, a tak ho chceme obnovit. Klepneme na tlačítko... a nic se nestane. Proč? Protože ignorujeme sílu odkazovaných hodnot.
Tato část: const currentValue = DEFAULTVALUE říká JS následující: převezměte odkaz na DEFAULT.VALUE a přiřadit jí proměnnou currentValue. Skutečná hodnota je v paměti uložena pouze jednou a obě proměnné na ni ukazují. Změna některých vlastností na jednom místě znamená jejich změnu na jiném místě. Máme několik možností, jak se takovým situacím vyhnout. Jedním z nich, který splňuje naše potřeby, je operátor spread. Opravme si náš kód:
const DEFAULT_VALUE = {
favoriteBand: 'The Weeknd'
};
const currentValue = { ...DEFAULT_VALUE };
const bandInput = document.querySelector('#favorite-band');
const restoreDefaultButton = document.querySelector('#restore-button');
bandInput.addEventListener('input', () => {
currentValue.favoriteBand = bandInput.value;
}, false);
restoreDefaultButton.addEventListener('click', () => {
currentValue = { ...DEFAULT_VALUE };
}, false);
V tomto případě funguje operátor spread takto: vezme všechny vlastnosti z objektu a vytvoří nový objekt, který je jimi naplněn. Díky tomu již hodnoty v currentValue a DEFAULT_VALUE neukazují na stejné místo v paměti a všechny změny aplikované na jednu z nich neovlivní ostatní.
Dobře, takže otázka zní: je to všechno o používání kouzelného operátoru spread? V tomto případě ano, ale naše modely mohou vyžadovat větší složitost než tento příklad. V případě, že použijeme vnořené objekty, pole nebo jiné strukturované objekty, operátor rozprostření odkazované hodnoty nejvyšší úrovně ovlivní pouze nejvyšší úroveň a odkazované vlastnosti budou stále sdílet stejné místo v paměti. Existuje mnoho řešení, jak se s tímto problémem vypořádat, vše závisí na vašich potřebách. Můžeme klonovat objekty v každé hloubkové úrovni nebo při složitějších operacích použít nástroje, jako je immer, který nám umožňuje psát neměnný kód téměř bezbolestně.
Vše smíchejte dohromady
Je použitelná kombinace znalostí o oborech a typech hodnot? Samozřejmě, že ano! Pojďme vytvořit něco, co využívá obojí:
const useValue = (defaultValue) => {
const value = [...defaultValue];
const setValue = (newValue) => {
value.length = 0; // záludný způsob vymazání pole
newValue.forEach((item, index) => {
value[index] = item;
});
// udělat nějaké další věci
};
return [value, setValue];
};
const [animals, setAnimals] = useValue(['cat', 'dog']);
console.log(animals); // ['cat', 'dog']
setAnimals(['horse', 'cow']);
console.log(animals); // ['horse', 'cow']);
Vysvětleme si, jak tento kód funguje řádek po řádku. No, funkce useValue vytváří pole na základě argumentu defaultValue; vytváří proměnnou a další funkci, její modifikátor. Tento modifikátor přebírá novou hodnotu, která je záludným způsobem aplikována na stávající hodnotu. Na konci funkce vrátíme hodnotu a její modifikátor jako hodnoty pole. Dále použijeme vytvořenou funkci - deklarujeme zvířata a setAnimals jako vracené hodnoty. Pomocí jejich modifikátoru zkontrolujeme, zda funkce ovlivňuje proměnnou animal - ano, funguje to!
Ale počkejte, co přesně je na tomto kódu tak fantastického? Odkaz uchovává všechny nové hodnoty a do tohoto modifikátoru můžete vložit vlastní logiku, jako např. některá rozhraní API nebo součástí ekosystému který bez námahy napájí tok dat. Tento záludný vzor se často používá v modernějších knihovnách JS, kde nám funkcionální paradigma v programování umožňuje udržet kód méně složitý a snáze čitelný pro ostatní programátory.
Souhrn
Porozumění tomu, jak funguje mechanika jazyka pod kapotou, nám umožňuje psát uvědomělejší a lehčí kód. I když JS není nízkoúrovňový jazyk a nutí nás mít určité znalosti o tom, jak se přiřazuje a ukládá paměť, stále musíme dávat pozor na neočekávané chování při úpravách objektů. Na druhou stranu zneužívání klonů hodnot není vždy správnou cestou a nesprávné použití má více nevýhod než výhod. Správným způsobem plánování toku dat je zvážit, co potřebujeme a na jaké možné překážky můžeme narazit při implementaci logiky aplikace.
Přečtěte si více: