ES6 v kostce

Nová verze JavaScriptu zvaná ES6 (ECMAScript 6) je k dispozici již skoro 7 let a za tu dobu se již dostala do všech překladačů (např. Node.js) a i všech moderních prohlížečů.

ES6 nepřináší příliš nových funkčností ale spíše se zaměřuje na to, aby se používání stávajících funkcionalit stalo snažší a aby nebylo potřeba psát tolik kódu (podle DRY). Co všechno tedy přináší nového v porovnání s předchozí verzí?

Jediné prohlížeče, které ES6 nepodporují (jednoduše proto, že jejich poslední verze byla vydána před uvedením ES6 v červnu 2015), jsou Internet Explorer (Edge 12+ ji podporuje), Opera Mini a prohlížeč v Blackberry (a nějaké obskurní prohlížeče jako QQ).

Poznámka: ECMAscript není to samé jako JavaScript. ECMAscript definuje základní pravidla, která pak přebírají jazyky jako JavaScript, TypeScript, apod. Dá se ale říct, že co platí v ES, platí i v JS.

Další poznámka: Výraz ES6 se původně používal pro 6. verzi ECMAscript, ale v současnosti označuje všechny verze následující po ES3 (2000+) a ES5 (2010+), tedy verze 6 (2015), 7 (2016) a 8 (2017), 9 (2018), 10 (2019), 11 (2020), 12 (2021), 13 (2022) a 14 (2023) – s tím, že do prohlížečů obvykle nové funkčnosti dorazí o 1 až 2 roky později. U každé položky budu uvádět potřebnou verzi, pokud je vyšší než ES6.

Proměnné

Kromě stávající definice proměnné pomocí var, která vytvoří proměnnou ve scope funkce můžete vytvořit proměnnou pomocí let, které nadefinuje proměnnou jen v daném bloku – např. IF, FOR, atd.

for (var i = 0; i < 10; ++i) { 
    console.log(i); 
} 
console.log('Cyklus skončil na ', i);  //i = 10 

for (let j = 0; j < 10; ++j) { 
    console.log(j); 
} 
console.log('Cyklus skončil na ', j); 
//vypíše chybu "j is not defined"

Pokud jste zvyklí definovat proměnné na začátku funkce, tak vám tohle, stejně jako mě, asi nepřijde moc přínosné, ale v opačném případě může lépe pomoci odhalit chyby, když se snažíte použít proměnnou, která je již definována, ale má neočekávanou hodnotu.

Konstanty

Třetí možností je definice konstanty pomocí klíčového slova const. Konstanta se chová stejně jako proměnná definovaná pomocí let s tím rozdílem, že do ní lze přiřadit hodnotu pouze při definici a jakékoliv další přiřazení vyvolá chybu „Assignment to constant variable„. Naopak pokud použijete const a neurčíte hodnotu, dostanete chybu „Missing initializer in const declaration„.

Konstanta může obsahovat všechny typy stejně jako normální proměnné a při jejich definici je také možné používat všechny možnosti jazyka jako jsou matematické výpočty, volání funkcí, atd.

Důležité je uvědomit si, že pokud konstanta obsahuje pole nebo objekt, nelze do ní sice přiřadit jiné pole nebo objekt, ale bez problémů lze měnit prvky pole a vlastnosti objektu!

const 
    a = [1,2,3], 
    o = {a: 1, b: 2, c: 3}; 

a[1] = 9; //a === [1, 9, 3] 
o.b = 9;  //o === {a: 1, b: 9, c: 3}

Scope konstanty je stejný jako u let, takže např. uvnitř cyklu můžete do konstanty přiřadit novou hodnotu v každé iteraci (ale jen jednou).

for (let i = 0; i < 10; ++i) {
     const power = Math.pow(2, i); //lze
//   power = 0; //nelze, vyhodí chybu
     console.log('Další mocnina 2 je', power);
 }

Konstantu ale nelze použít pro řídící proměnnou cyklu FOR, protože tu měníte sami:

//NEPOUŽÍVAT!!!! 
for (const i = 0; i < 10; ++i) {
     console.log(i);
     //vypíše pouze 0 a pak chybu, že nejde změnit 
}

Klíčové slovo const se také hodí pro vytváření funkcí metodou přiřazení anonymní funkce do proměnné. Díky tomu, že funkci nadefinujete jako konstantu, zamezíte tomu, aby jste si ji později nechtěně přepsali:

//starý zápis
var process = function(input) { ...; return result; }

//ES6
const process = (input) => { ...; return result; }

Stejně můžete přiřadit i výsledek IIFE (samo-spustitelné funkce), který by se také neměl měnit:

//starý zápis IIFE
var isOk = (function() { ...; return status;})();

//ES6 a IIFE
const isOK = (() => { ...; return status;})();

Destrukturalizace objektů

Při definici proměnných (var, let a const) můžete vlastnosti objektu přiřadit do jednotlivých proměnných. Pokud používáte PHP, tak jistě znáte konstrukci list(), která funguje podobně:

var user = {id: 123, name: "john", age: 30}

//vytvoří proměnné id a name a naplní je z user: 
var {id, name} = user; 

//vytvoří proměnnou firstName se jménem z user: 
var {name: firstName} = user;

Proměnné můžete buď pojmenovat stejně jako původní vlastnosti objektu nebo je přejmenovat uvedením dvojtečky a nového jména.

Stejný zápis samozřejmě můžete použít u v případě, kdy na pravé straně (za rovnítkem) je volání funkce, která vrací objekt.

Získat z objektu můžete i hlouběji zanořenou vlastnost tím, že uvedete její cestu tak, jako byste vytvářeli daný objekt:

//starý zápis 
var name = getUser().default.firstName; 

//ES6 
var { details: { firstName: name }} = getUser();      //vytvoří pouze proměnnou name

Pokud vlastnost v objektu neexistuje, nastaví se příslušná proměnná na hodnotu undefined. Vy ale můžete zadat vlastní výchozí hodnotu:

var 
    user = {},
    { name = 'Unknown', age = 0} = user
;

Objekt můžete destrukturovat i pokud je uveden jako parametr funkce. Daná funkce pak bude mít více parametrů, než kolik jich předáte:

function createUser(id, {name: firstName, age}) {
     return {id, firstName, age};
}
var 
    simpleUser = {name: 'john', age = 30},
     user = createUser(123, simpleUser) 
;

Destrukturalizace polí

Stejně jako u objektů můžete vytvářet proměnné z prvků pole:

var
     a = [1,2,3],
     [b, c, d] = a //b == 1, c == 2, d == 3
;

Pokud chcete některé prvky pole přeskočit, stačí uvést prázdné jméno (tedy dvě čárky za sebou). Také můžete zadat výchozí hodnotu pro indexy, které v poli nejsou definovány:

var 
     a = [1,2,3],
     [b, , c, d = -1] = a //b == 1, c == 3, d == -1
;

Díky tomuhle zápisu můžete snadno prohodit dvě proměnné tím, že z nich vytvoříte pole a pak ho destrukturalizujete (tento zápis je ale pomalejší než s pomocnou proměnnou!):

var  a = 1, b = 2; 

//starý zápis - rychlejší
var tmp = b;
b = a;
a = tmp;

//ES6 - pomalejší, ale přehlednější
[a, b] = [b, a]; //a == 2, b == 1

Stejně jako objekt můžete pole destrukturovat, pokud je zadáno jako parametr funkce:

function sum([a, b = 0, c = 0, d = 0]) {
     return a + b + c + d; 
} 

var arr = [1, 2, 3]; 

console.log(sum(arr)); // vypíše 6 (tedy 1+2+3)

Definice řetězce

Kromě klasické definice řetězce pomocí jednoduchých a dvojitých uvozovek můžete použít definici template pomocí znaku backtick (česky těžký akcent), který můžete znát z Linux konzole nebo MySQL. Na české klávesnici ho napíšete stiskem pravého ALT a ý (resp. číslo 7).

Template má oproti klasickému řetězci dvě výhody: 1) může obsahovat odřádkování (které pak do řetězce vloží znak \n) a 2) může obsahovat proměnné podobně jako dvojité uvozovky v PHP a výrazy podobně jako Linux konzole.

var
     a = 5,
     example = `Tenhle text může být na více řádkách, obsahovat znak backtick: \`, vkládat proměnné: ${a} a výsledky výrazů: ${Math.max(3, a)}`;

Zajímavou vlastností je to, že JavaScript kompilátor musí být schopen odlišovat scope samotné template a vnitřního výrazu, takže template uvnitř výrazu není potřeba escapovat (resp. by to bylo špatně):

//tohle je potřeba escapovat: 
var equal = eval('5==\'5\'?\'OK\':\'Chyba\''); 

//tady žádný escape potřeba není: 
var equal = `${5 == `5` ? `OK` : `Chyba` }`;

Další možností použití template je předání parametrů do funkce (tzv. tagged template), která se pak zavolá místo vyhodnocení řetězce:

function equal(strings, number, string) {
     if (number == string) {
         return strings[0];
     }
     else {
         return strings[1];
     }
}

//tohle je vážně podivný zápis (ale je správně): 
var result = equal`OK${2+3}Chyba${`5`}.`;
console.log( result ); //vypíše OK protože 2+3 == "5"

Funkce jako první parametr dostane pole všech řetězců, které byly v template nalezeny (zde „OK“, „Chyba“ a „.“) a následně výsledek každého výrazu jako jeden parametr (zde 5 a „5“).

První parametr má (ale až od ES9!) ještě skrytou vlastnost strings.raw[], ve kterém jsou uloženy řetězce přesně tak, jak byly zadány včetně escapovacích znaků a UNICODE znaků zadaných pomocí \uXXXX.

Definice objektu

Objekt definujete klasicky {vlastnost: hodnota}, kde jako hodnotu můžete uvést proměnnou. U ES6 ale již nemusíte uvádět jméno vlastnosti, pokud ji chcete uložit do stejné vlastnosti jako je jméno proměnné:

var 
    name = 'john', 
    age = 30, 
    id = 123,
    user = {id, name, age} 
;
//user == {id: 123, name: "john", age: 30}

Pokud chcete zkopírovat vlastnosti jednoho objektu do druhého, musíte je nejprve uložit do proměnných:

var user == {id: 123, name: "john", age: 30}; 

let {name, age} = user; 

var simpleUser = {name, age};
//simpleUser == {name: "john", age: 30}

Pokud naopak potřebujete jméno vlastnosti vypočítat nebo zadat z proměnné, v předchozí verzi JavaScriptu bylo potřeba nejprve vytvořit objekt a pak teprve do něj vkládat vlastnosti. Nyní stačí jméno vlastnosti uvést do hranatých závorek:

//původní zápis 
var 
    name = 'name', 
    role = 'admin',
    o = { id = 123 };

o[name] = 'john';
o['right_' + role] = true;

//ES6
var
    name = 'name',
    role = 'admin',
    o = {
         id = 123,
         [name] = 'john',
         ['right_' + role] = true
    }
;
//o == {id: 123, name: "john", right_admin: true}

Funkce (metody) můžete v objektu vytvářet přímo bez použití přiřazení a klíčového slova function. Vytvářet můžete i virtuální vlastnosti, které volají funkci (getter a setter). Tento zápis je shodný v definicí třídy z ES6 (viz dále) a proto ho zde uvádím pro úplnost, ale pro objekty byl nadefinován již v ECMAscript 5.1.

//původní zápis 
var list = {
     items: [],
     add: function(item) {
         list.items.push(item);
     },
     count: function() {
         return list.items.length;
     }
};

//zkrácený zápis 
var list = {
     items: [],
     add(item) { list.items.add(item) },
     get count() { return list.items.length },
}
//poznámka: původní zápis má volání list.count();
//zatímco zkrácený jen list.count;
//nejde tedy o zcela ekvivalentní zápis

Roztažení (spread operator)

ES6 také podporuje operátor pro roztažení parametrů stejně jako třeba PHP 5.6 (kde se to nazývá argument unpacking). Operátor můžete využít jak při definici proměnné typu pole nebo objekt, tak i při volání funkcí.

Poznámka: Operátor roztažení pro objekty byl dokončen až ve verzi ECMAscript 2018, takže některé starší kompilátory a prohlížeče (před rokem 2018) ho nepodporují!

Volání funkce s parametry v poli

Dříve, pokud jste měli všechny parametry v poli (např. arguments) a chtěli jste s nimi zavolat funkci, museli jste použít metodu apply. Nyní již nemusíte:

(function() {
     //starý zápis:
     myFunc.apply(window, arguments);

     //nový zápis:
     myFunc(...arguments)
})(1,2,3,4,5);

//složení parametrů z více polí: 
myFunc(1, 2, ...arr1, ...arr2);

Pokud jako roztažený parametr použijete objekt, budou všechny jeho vlastnosti předány do funkce ve stejném pořadí, jako kdyby jste je získávali cyklem FOR-IN.

Poznámka: pamatujte, že uvnitř šipkové funkce není pole arguments k dispozici, takže na použití výše uvedeného kódu je potřeba zapsat celé function() {...} místo ()=>...!

Nově lze díky operátoru roztažení použít pole i pro konstruktory, což dříve nešlo (zkombinovat operátor new a metodu apply()):

//Ajax handler: 
function(response) {
     var user = new User(...response.user);
}

class User {
     constructor(id, name, age) {
         //...
     }
}

Připojení pole (Concat)

Operátor roztažení můžete použít pro navazování polí jednoduše tak, že druhé pole uvedete do prvního s použitím operátoru:

//vložení jednoho pole do druhého 
var
     arr1 = [1, 2, 3, 4, 5],
     arr2 = [0, ...arr1, 6]
; //arr2 == [0, 1, 2, 3, 4, 5, 6]

//merge dvou polí
var
     arr1 = [1, 2, 3],
     arr2 = [4, 5, 6],
     mergeArr = [...arr1, ...arr2]
; //mergeArr == [1, 2, 3, 4, 5, 6]

Spojení objektů (Merge) [ES9]

U objektů operátor roztažení vkládá indexy ve stejném pořadí jako jsou definovány v jednotlivých objektech a pokud se opakují, tak přepíše jejich hodnoty:

var
     user = {id: 123, name: 'john', age: 30},
     employee = {id: 456, name: 'doe',
                 salary: 5000},
     person = {...user,
               ...employee,
               id: undefined
     }
;
//person == {
//    id: undefined,
//    name: 'doe',
//    age: 30,
//    salary: 5000}

Všimněte si, že v objektu person je id jako první, protože bylo první v objektu user, ale má hodnotu undefined, což jsme definovali na konci objektu person. Stejně tak vlastnost name je 'doe', protože hodnota z employee přepsala tu z user.

Kopírování pole

Ve výchozím přiřazení se pole předávají referencí, což znamená, že přiřazení pole do nové proměnné a následná změna, změní i pole původní. Pokud chcete vytvořit nezávislou kopii, můžete k tomu použít operátor roztažení:

//uložení reference bez kopie
var
    a = [1, 2, 3],
    b = a
;
b[1] = 5; // a == [1, 5, 3]

//vytvoření kopie pole
var
    a = [1, 2, 3],
    b = [...a]
;
b[1] = 5; //a == [1, 2, 3]

Toto ale platí jen pro primitivní typy. Pokud jsou v poli objekty nebo jde o víceúrovňové pole, druhá úroveň se opět předá referencí:

//vytvoření kopie pole s objekty
var
    a = [{x:1}, {y:2}, {z:3}],
    b = [...a]
;
b[1].y = 5; //a == [{x:1}, {y:5}, {z:3}]

Bezpečné řetězení [ES11]

Jednou z velkých výhod Javascriptu je možnost řetězení vlastností nebo funkcí (tato možnost je běžná u objektových jazyků, i když ES je původně funkcionální). Naopak nevýhodou tohoto přístupu je ten, že v JS nevíte, jestli vlastnost existuje nebo jestli funkce vrátí něco vhodného k řetězení (např. u PHP se toto dá zajistit tím, že funkci přidáte návratový typ :self).

ES11 proto definuje nový operátor ?. (optional chaining, ale funguje stejně jako NULL-coalescing operátor popsaný níže), který zajistí, že pokud je daná vlastnost NULL nebo funkce vrátí NULL, tak se následující řetěz již nezpracuje.

//starý příklad - bez ošetření
let value = window.myScope.api.getData().value;
//může vyvolat chybu, pokud myScope nebo api neexistují
//nebo getData() nevrátí očekávaný objekt

//starý příklad s ošetřením
let data = (window.myScope && window.myScope.api
            && window.myScope.api.getData
            && window.myScope.api.getData());
let value = data.value || undefined;

//ES11
let data = window.myScope?.api?.getData()?.value;

Zajímavé je, že stejný operátor jde použít i na pole a funkce, kde za ním následuje složená nebo kulatá závorka:

window.jQuery?.ajax?.('/api/getData').done(...);
//stáhne data, ale jen pokud je jQuery načtené
//a má k dispozici funkci ajax()


$('input')?.[0]; //vrátí první input ve stránce
   //... nebo undefined pokud žádný input neexistuje

Pozor na to, že tento operátor nefunguje na první položku v řetězu, ať už je to objekt, pole nebo funkce. Pokud tedy chcete ověřovat vlastnosti z globálního scope (např. window), je potřeba ho napsat:

let value = window?.value; //ReferenceError,
      //... pokud nejsme v prohlížeči
let first = a?.[0]; //ReferenceError,
      //... pokud globální scope nemá vlastnost 'a'
let first = window.a?.[0]; //Toto je OK,
      //... vrátí undefined pokud window.a neexistuje

sendData?.(data); //ReferenceError, 
      //... pokud sendData() neexistuje
window.sendData?.(data); //OK, data se pošlou nebo ne

Operátor také nejde použít pro přiřazení hodnoty, protože tam je potřeba, aby celý řetěz existoval (poslední hodnota samozřejmě existovat nemusí a vytvoří se):

window.jQuery?.ajaxSettings?.contentType = 'text/JSON';
//... vždy vrátí "SyntaxError: Invalid left-hand
//... side in assignment", protože jQuery i ajaxSettings
//... musí existovat, aby šlo přiřadit hodnotu.

Ještě poslední poznámka: operátor ve potřeba napsat dohromady jako „?.“; není možné ho rozdělit, i když by se to mohlo zdát logické (protože v tom případě jde o chybnou kombinaci ternárního a objektového operátoru):

//ŠPATNĚ - NEPOUŽÍVAT!!!
let value = window.myScope?
     .api? //SyntaxError: unexpected token '.'
     .getData()?
     .value
;

//SPRÁVNĚ
let value = window.myScope
     ?.api
     ?.getData()
     ?.value
;

Export a import

Proměnné (včetně objektů a tříd) můžete vkládat do souboru z jiného pomocí klíčových slov import a export. Ta nahrazují dříve používané konstrukce CommonJs (module.exports) a AMD (define()). Skripty s export se nazývají moduly.

Pojmenované exporty

Pokud chcete použít proměnnou definovanou v jiném souboru, musíte určit jméno proměnné a jméno souboru (ideálně relativně k současnému souboru) pomocí import ... from '...':

import { arr1, obj1, f1, class1 }
         from 'variables.js';

Jméno souboru může začínat bez lomítka nebo s ‚./‚ (aktuální složka nebo podsložka) nebo ‚../‚ (nadřazená složka). Přípona může, nemusí nebo nesmí být uvedena – záleží to na prostředí, ve kterém budete skript spouštět (Node.js, prohlížeč, apod.). V některých prostředích můžete např. importovat proměnné z HTML souborů (viz dále). Jméno souboru (modulu) musí být definováno jako statický řetězec, nejde použít template `...`, proměnné, volání funkce, apod. (viz cyklické importy).

V daném souboru pak požadované proměnné označíte pomocí slova export:

//export nové proměnné a funkce
export const arr1 = [1, 2, 3];
export let obj1 = { a: 1, b: 2};
export function f1() { //... }

//export dříve definované proměnné nebo třídy
var f2 = function() { //... };
export {f2};

class User { //... }
export {User}; 

//Export pod jiným jménem
export {f2 as process};
export {User as MyUser};

//export více proměnných najednou
export {arr1, obj1, f1, f2 as process, User}; 

Importované proměnné můžete také přejmenovat:

import { arr1 as a, obj1 as o }
              from 'variables.js';

Všimněte si, že pokud exportujete dříve definovanou proměnnou (funkce, třídu, apod.), musíte ji uvést do složených závorek. Stejně tak musíte danou proměnnou uvést do složených závorek za klíčovým slovem import. Uvedení export jmeno je neplatná syntaxe!

Výchozí export

Aby JavaScript umožňoval importování tříd podle OOP (tedy jméno souboru se má jmenovat stejně jako třída, kterou definuje, a každý soubor má definovat jen jednu třídu), můžete použít výchozí export, který určí, co je hlavní exportovaná proměnná (nebo třída) daného souboru:

//Database/User.js
class User { //... }
export default User;

//main.js
import DbUser from 'Database/User';

Všimněte si, že výchozí import se liší tím, že jméno neuvádíme do složených závorek! Ve výchozím importu určujete pouze jméno, pod kterým se uloží do aktuálního skriptu, ale nemusí být stejné jako jméno v původním souboru.

Poznámka: výchozí export je ve skutečnosti export proměnné pod jménem default.  Pokud tedy použijete výchozí export a zároveň vyexportujete další proměnnou pod jménem default, dojde k jejich přepsání. Stejně tak zápis import name from ... je zkrácené import {default as name} from ....

//value.js
var default = 2, value = 3;

export default 1; //import value == 1
export default;   //import value == 2
export {value as default}; //import value == 3

Moduly nejsou globální

Pokud skript vložíte jako modul, jeho globální scope bude oddělen a globální proměnné a funkce vytvořené v modulu se neuloží do globálního prostoru (namespace) aktuálního skriptu.

// ..........  lib.js ..............
// globální funkce
function process() { ... }
// globální proměnná
var result = {};
//export
export {process as libProc, result as libRes};

// .......... main.js ..............
import {libProc, libRes} from 'lib';

console.log(process); //undefined, není globální
console.log(result);  //undefined, není globální

Skripty, které tedy obsahují exporty a očekává se, že se budou načítat pomocí import, nemusíte obalovat do self-invoking funkce, abyste zabránili zamoření globálního scope:

// .......... lib.js ..............
;(function() { //není potřeba pro modul
    function process() { ... }
    var result = {};
    export {process as libProc,
            result as libRes};

})(); //není potřeba pro modul

Pokud byste chtěli něco uložit přímo do globálního namespace, můžete použít klíčové slovo daného prostředí (např. window v prohlížeči, GLOBAL v Node.js, apod.). Ukládat ale data do globálního prostoru není příliš dobrá technika (ve spojení s moduly).

Od ES10 můžete použít globalThis, které odpovídá jak window v prohlížeči tak i global v Node.JS. Pokud tedy potřebuje modul sdílet mezi různými prostředímy, toto je jednoduhá varianta.

Předávání importů

Exportovat můžete i dříve importované proměnné, takže jednotlivé soubory mohou mezi sebou sdílet data:

//Database.js
import connection from 'Connection';
import {server} from 'Config';

connection.connect(server);

export default connection;
export server;

//main.js
import Db, {server} from 'Database';

try {
    Db.query('SELECT ... ');
} catch (e) {
    console.log('Failed query to', server);
}

Všimněte si, že z jednoho souboru můžeme importovat současně výchozí proměnnou a další pojmenovanou (které jsme navíc obě naimportovali každou z jiného souboru).

Pokud chcete rovnou exportovat proměnné z jiného souboru, můžete použít speciální konstrukci export {...} from '...' nebo pro úplně všechny proměnné export * from '...':

//beta.config.js
export * from 'production.config.js';
export {services, plugins} from 'framework';

import {server} from 'production.config.js';
server = 'beta.' + server;
export server;

Cyklické importy

Pokud máte dva provázané soubory, můžete je mezi sebou navzájem naimportovat podle potřeby. To je díky tomu, že JavaScript nejprve projde kód a zkompiluje ho a teprve následně ho spouští. Proto také musí být jméno souboru nadefinováno jako statický řetězec, protože v době, kdy se jméno vyhodnocuje a soubor se načítá, ještě nedochází k vyhodnocení obsahu proměnných ani výsledků funkcí nebo templatů.

//Database.js
import connection from 'Connection';
import {server} from 'main';

connection.connect(server);

export default connection;
export server;

//main.js
export var server = 'localhost';
import Db from 'Database';

try {
    Db.query('SELECT ... ');
} catch (e) {
    console.log('Failed query to', server);
}

Dynamické (asynchronní) importy [ES10]

Importy v ES6 fungují synchronně – tedy po zavolání import {} from '' se čeká, až se soubor stáhne a zkompiluje.

Od ES10 máte ale místo konstruktu import k dispozici funkci import(), který vrací Promise. Můžete díky tomu počkat na knihovnu asynchroně, pokud ji potřenujete jen k jednomu výpočtu nebo volání:

//asynchronně odešle data na API
import('/modules/api.js')
    .then((api) => api.sendData(data))
;

//synchronně stáhne data z API
let data = await import('/modules/api.js')
    .then((api) => api.loadData())
;

Všimněte si, že na rozdíl od import {} from “, kde uvádíte, které funkce nebo proměnné chcete nahrát do globálního scopu (a volitelně pod jakým jménem), tak funkce import() vždy vrátí celý modul (stejně jako výchozí import import name from 'file'), ale neukládá ho do globálního scopu (a místo toho ho vrátí jako odpověď Promisu).

Export knihovny

Pokud máte knihovnu, která exportuje více funkcí, nemusíte je importovat podle jmen, ale můžete si je všechny uložit do jedné proměnné:

//Numbers.js
export toBinary = function(i) { ... }
export toHexa = function(i) { ... }
export const PI = 3.14;
//... atd.

//main.js
import * as Num from 'Numbers';
var bits = Num.toBinary(255),
    circle = Num.PI * 1 * 1;

Importy v HTML

V prostředích typu Node.js fungují importy tak, že zatímco se čeká na načtení a zkompilování importovaného souboru, pozastaví se současný skript, ale místo něj může běžet jiný skript.

V HTML probíhá ale všechno synchronně, takže zatímco se čeká na stažení souboru, stojí provádění JavaScriptu i renderování DOMu.

Aby bylo možno zpracovávat importy asynchronně, můžete použít nový HTML tag module (nebo <script type="module">:

<script type=module>
    import $ from 'jquery';
    $('.highlight').css('background', 'yellow');
</script>
<script type=module>
    import $ from 'jquery';
    $('.highlight').css('background', 'yellow');
</script>

Modul se zpracovává stejně jako deferred skript, tedy je spuštěn až v okamžiku, kdy nemá prohlížeč nic jiného na práci. Modul v HTML má pak samozřejmě i ostatní vlastnosti ES6 modulu, jako že nezamořuje svými proměnnými globální namespace window.

Pokud chcete, aby se modul spustil ihned jak bude zpracován (nebo stažen), můžete přidat async:

<script async type=module">
    import $ from 'jquery';
    $('.highlight').css('background', 'yellow');
</script>
<script async type=module>
    import $ from 'jquery';
    $('.highlight').css('background', 'yellow');
</script>

Kód modulu lze také stáhnout ze souboru a dokonce existuje i alternativa pro prohlížeče, které moduly ještě nepodporují:

<script type=module src="highlight.js" />
<script nomodule src="highlight.js">

Výhoda modulu je v tom, že pokud se vám ho podaří do HTML vložit vícekrát (např. z různých šablon nebo přes AJAX), spustí se vždy jen jednou (podobně jako import_once v PHP) – alespoň teoreticky, některé verze prohlížečů Firefox a Safari mohou spustit současně modul a jeho nomodule náhradu a Edge zatím spouští moduly stejně jako skripty, tedy tolikrát, kolikrát je najde v kódu.

Moduly se pro urychlení stahují bez odesílání COOKIES. Pokud ale potřebujete poslat COOKIE pro přihlášení, můžete přidat atribut crossorigin="use-credentials".

Cykly

For each

Stávající javascript již nabízí cyklus FOR-IN, který do proměnné ukládá indexy. Nově ale můžete použít FOR-OF, který naopak ukládá hodnoty (a více tak odpovídá FOREACH z PHP nebo $.each z jQuery). V případě, že procházíte pole nebo objekt a nezáleží vám na indexech nebo jménech proměnných, je tohle lepší volba:

var arr = [1,2,3];

for (const i in arr) {
    console.log(arr[i]); //musíme číst z pole
}

for (const value of arr) {
    console.log(value); //máme přímo hodnotu
}

Oba zápisy (FOR-IN a FOR-OF) mohou iterovat pole (vrací prvky), objekty (vrací vlastnosti a funkce) a řetězce (vrací jednotlivé znaky).

Všimněte si, že u cyklů FOR-IN a FOR-OF má konstanta platnost pouze pro jednu iteraci – toto je doporučený postup, abyste si během cyklu neměnili hodnotu iterace a uvědomili si, že u FOR-OF nelze změnit hodnotu původní proměnné, kterou iterujete.

For await (ES9)

Pokud máte pole nebo objekt, který obsahuje Promisy (viz dále), nemůžete je jednoduše procházet, protože nemusí být ještě dostupná odpoveď (pokud netušíte, o čem mluvím, přeskočte nejprve na kapitolu Promise).

Samozřejmě byste mohli použít FOR-OF a uvnitř volat let result = await i;, ale smyslem novějších ES je práci usnadňovat, takže proto tu máme cyklus FOR-AWAIT (což je nadstavba FOR-OF), který to udělá za vás. V proměnné tak již dostanete odpověď, na kterou cyklus sám počká.

for await (const response of processes) {
    handle(response);
}

Zajímavé je, že FOR-AWAIT nemusí zpracovávat jen pole Promisů, ale zvládne jakékolik pole nebo objekt. Pokud si tedy jste jisti, že máte k dispozici ES9 a naopak 100% nevíte, co dostatene jako vstup FOR-OF, můžete bez problémů místo toho použít FOR-AWAIT i když přímo nehodláte zpracovávat Promisy.

Funkce

O šipkových funkcích a yield generátorech jsem již psal dříve.

Rest operátor

Spread operátor můžeme použít pro předání parametrů do funkce z pole. Rest operátor vypadá stejně, ale funguje opačně (stejně jako variadické funkce v PHP 5.6):

var myFunc = function(...args) {
    for (arg of args) {
        //...
    }
};

myFunc(1,2,3,4,5);

Výhoda rest operátoru oproti použití arguments je v tom, že rest operátor vytvoří skutečné pole (zatímco arguments je jen objekt s vlastností length a nelze tedy na něj použít metody pole) a navíc je dostupný i v šipkových funkcích (což arguments není).

Samozřejmě lze použít i v konstruktoru:

class User {
    constructor(...values) {
        this.values = [];
        for (value of values) {
            this.add(value);
        }
    }
    add(value) { this.values.push(value); }
}

Výchozí hodnota parametru

Spousta JS funkcí provádí něco podobného:

//starý zápis
function(list, options) {
    list = list || [];
    options = options || {};
    return list.map(function(i) {
        return options.prefix +
                   i + options.suffix;
    }
}

Tedy zajištění, že vstupní parametry budou vždy definované na očekávaný typ (string, array, object, apod.), abychom při každém přístupu k jejich vlastnostem nebo metodám nemuseli ověřovat, zda je to možné.

ES6 tohle usnadňuje pomocí výchozích hodnot pro nedefinované parametry:

function(list = [], options = {}) {
    return list.map(i => options.prefix + i
                       + options.suffix
    );
}

Výchozí hodnota samozřejmě neřeší to, že jako první parametr očekáváte pole, ale programátor může předat objekt nebo string.

Čárky na konci [ES8]

V ES8 se již nemusíte bát nadbytečných čárek na konci seznamu parametrů (a vlastně i kdekoliv jinde):

function obj(a, b, c,) { //čárka za c je OK
    return {a, b, c,}; //čárka za c zase OK
}
let o = obj(1, 2, 3,) //čárka za 3 je taky OK
//pro úplnost: o == {a: 1, b: 2, c: 3}

Třídy

ES6 konečně implementuje klíčové slovo class (které bylo celé roky rezervované, ale nic nedělalo).

Důležité je uvědomit si, že na rozdíl od proměnných (a funkcí), musí být třída definována dříve než ji použijete! Třídy se tedy chovají podle OOP a ne jako funkcionální JavaScript.

Třídu můžete definovat, stejně jako funkci, dvěma způsoby:

//pojmenovaná globální třída
class User {}

//anonymní lokální třída
let User = class {};

//třída definovaná v modulu
export default class {}

Konstruktor a metody

Třída může (ale nemusí) obsahovat konstruktor (který odpovídá původnímu funkcionálnímu zápisu, kde konstruktorem byla funkce pojmenovaná jako daná třída) a také další metody a dokonce i statické metody:

class User {
    constructor(id, name) {
        this.id = id;
        this.name = name;
    }
    //metody
    login(password) { //... }

    //statické metody
    static factory(user) {
        return new User(user.id, user.name);
    }
}

var
    user1 = new User(1, 'john'),
    user2 = User.factory({id: 2, name: 'doe'})
;

user1.login('123456');

Všimněte si, že metody se nedefinují klíčovým slovem function ale pouze jménem a parametry v závorce. Konstruktor má pak jméno constructor() místo někdy používaného jména třídy.

Poznámka: na rozdíl od jazyků jako je PHP nelze volat statické metody z instance (např. user1.factory()) a statické metody mají proměnnou this nastavenou na undefined, takže její použití vyvolá výjimku (rozdíl oproti původnímu funkcionálnímu zápisu, kde šlo funkci volat i bez operátoru new).

Vlastnosti třídy

ES6 neumí definovat veřejné (public), soukromé (private – ES11 to umí, viz dále) ani statické proměnné (jako součást definice třídy).

Veřejné vlastnosti můžete nadefinovat v konstruktoru, statické stejně jako jste byli zvyklí do teď (tedy až po nadefinování třídy):

class User {
    constructor(age) {
        this.age = Math.max(age, User.minAge);
    }
}
User.minAge = 18;

var
    user1 = new User(30),
    user2 = new User(13)
; 

console.log(user1.age, user2.age); // 30 18

user2.age = 13;
console.log(user2.age); // 13

Jediný způsob, jak vytvořit privátní proměnnou v ES6 je stejný jako dosud pomocí uzávěry v konstruktoru:

class User {
    constructor(age) {
        var _age = Math.max(age, User.minAge);

        this.getAge = () => _age;
        this.setAge = (age) => {
            _age = Math.max(age, User.minAge);
        }
    }
}
User.minAge = 18;

var user1 = new User(30),
    user2 = new User(13); 

console.log(user1.getAge()); // 30

user2.age = 13;
console.log(user2.age, user2.getAge()); // 13 18

Privátní vlastnosti [ES11]

Staré nepsané pravidlo bylo, že privátní vlastnosti objektů začínají podtržítkem; nicméně i tak byly viditelné (public) a šlo je v případě potřeby použít. Aby ES tuto pravidlo nerozbilo, přidává možnost definovat vlastnosti s hash znakem (#) a učinit je tak přístupné pouze uvnitř třídy.

class User {
    #first_name;
    #last_name;
    constructor(first, last) {
        this.#first_name = first;
        this.#last_name = last;
    }
}

let u = new User('Jan', 'Novák');
u.first_name; //== undefined, protože vně neexistuje
u.#first_name; // SyntaxError: privátní vlastnost

Gettery a Settery [ES6]

Vlastnost třídy můžete vrátit pomocí getteru nebo nastavit pomocí setteru. Problém je ale v tom, že když getter nebo setter neuvedete, nevyvolá se žádná chyba, pokud se o přečtení nebo uložení pokusíte a příkaz se bude pouze ignorovat.

class Dice {
    get value() {
        return Math.round(Math.random()*6+1);
    }
}

var kostka = new Dice();
console.log(kostka.value); //náhodně 1 až 6
kostka.value = 7; //nic nehlásí

Navíc pokud uvedete getter nebo setter, nelze se již dostat ke stejnojmenné veřejné vlastnosti daného objektu, protože jakékoliv zavolání jména vlastnosti znovu vyvolá getter nebo setter (i rekurzivně):

class Dice {
    //NEPOUŽÍVAT!!!
    constructor() {
        this.value = Math.round(
                       Math.random()*6+1);
    }
    get value() {
        return this.value;
    }
    set value(i) {
        throw Error('Nepodváděj!');
    }
}

var kostka = new Dice();
console.log(kostka.value); //stack overflow

Tento kód by fungoval v libovolném jiném jazyce, ale v JavaScriptu dotaz na kostka.value bude dokola volat Dice::value() dokud nespadne. Navíc i nastavení vlastnosti v konstruktoru vyvolá setter, takže hned vytvoření instance vyhodí výjimku!

Gettery a settery se tedy hodí pouze v případě, že chcete vrátit vypočtenou hodnotu nebo část jiné vlastnosti (pole, objekt, apod.):

class App {
    constructor() {
        this.log = [];
    }
    set next(x) { this.log.push(x); }
    get first() { return this.log[0]; }
}

var a = new App;
a.next = 'Start';
console.log(a.first); //'Start'
console.log(a.log);   //['Start']

Gettery a Settery nejsou ale výsada jen tříd, ale můžete je použít i při definici objektů (místo do teď složitějšího zápisu přes Object.defineProperty()). U objektu ale nemůžete používat klíčové slovo this pro přístup k ostatním vlastnostem ale musíte použít uzávěru:

const dice = {
    min: 1,
    max: 6,
    get roll() {
        return Math.round(
           Math.random() * dice.max + dice.min
        );
    }
}

console.log(dice.roll); //náhodně 1 až 6
dice.roll = 7; //ignoruje se

dice.max = 10; //vlastnosti konstanty lze měnit
console.log(dice.roll); //náhodně 1 až 10

Gettery a Settery [ES11]

ES11 přináší skutečné privátní vlastnosti, což mění způsob, jakým můžete dělat gettery a settery – již totiž neplatí, že nemůžete mít současně veřejný getter nebo setter a zároveň stejnojmennou vlastnost (ať už soukromou nebo veřejnou).

class User {
    #first_name;
    #last_name;
    constructor(first, last) {
        this.#first_name = first;
        this.#last_name = last;
    }
    get first_name() {
        return this.#first_name;
    }
}

let u = new User('Jan', 'Novák');
u.first_name; //== 'Jan' (prečteno getterem)
u.#first_name; // SyntaxError: privátní vlastnost

i.first_name = 'Honza'; //ignoruje se, nemá setter
i.#first_name = 'Honza'; // SyntaxError: privátní

Pozor na to, že zatímco u veřejných (public) vlastností je můžete nadefinovat v kontruktoru. tak u skrytých (private) je musíte definovat již v těle třídy:

class User {
    count = 0; //statická vlastnosti
    #first_name; //privátní vlastnost
    #role = 'User'; //privátní statická
    constructor(first, last, age) {
        ++this.count; //změna statické vlastnosti
        this.age = age; //veřejná vlastnosti
        this.#first_name = first; //skrytá vlastnost
        this.#last_name = last; //Syntax error
    }
    set role(role) {
        this.#role = role; //vytvoří novou privátní
        //vlastnost třídy, která přepíše tu statickou!
    }
}

V příkladu je age veřejná vlastnosti vytvořená v konstruktoru. Vlastnost #first_name je soukromá, protože je tak definována ve třídě. Vlastnosti #last_name ale vyvolá chybu, protože se v konstruktoru snažíme přiřadit hodnotu do soukromé vlastnosti, které nebyla nadefinována.

Dejte si tedy velký pozor na to, že veřejné vlastnosti se definují v kontruktoru zatímco veřejné statické proměnné se definují ve třídě, ale skryté vlastnosti je potřeba nadefinovat VŽDY ve třídě, ať už s ní chcete pracovat jako se statickou (má výchozí hodnotu) nebo vlastností instance (ne-statickou -> hodnota se přiřadí v konstruktoru nebo metodě).

Dědění

Třídy v ES6 mohou samozřejmě dědit od předků. Trochu rozdíl je v tom, že dědit lze pouze od tříd a ne již od objektů jako tomu je v případě prototypů. Pokud třídě chcete nastavit jako předka objekt, musíte použít metodu Object.setPrototypeOf().

class Member extends User {
    membershipEnds() { //... }
}

var Dump = {
    dump: function() {
        console.log(this);
    }
}
Object.setPrototypeOf(User.prototype, Dump);

var m = new Member();
m.dump(); //vypíše objekt Member do konzole

Pro zavolání metody předka můžete použít klíčové slovo super stejně jako používáte this:

class VipMember extends Member {
    membershipEnds() {
        return super.membershipEnds() + 90;
    }
}

Pokud potřebujete zavolat konstruktor předka, provede to tak, že super použijete jako metodu (náhražka za dříve používané Class.prototype.constructor.call(this)):

class VipMember extends Member {
    constructor(id, name, extension) {
        super(id, name);
        this.extension = extension
    }
    membershipEnds() {
        return super.membershipEnds() +
                this.extension;
    }
}

Mix-in

ES6 podporuje Mix-iny podobně jako PHP podporuje Traity nebo jazyk C vícenásobné předky. Zápis je ale jiný:

//Definice mixinu VIP,
//... který rozšiřuje třídu Member
var Vip = Member => class extends Member {
    membershipExtension() { return 90; }
}

//Použití mix-inu VIP na třídu Member
class VipMember extends Vip(Member) {
    membershipEnds() {
        return super.membershipEnds() +
             this.membershipExtension();
    }
}

Všimněte si, že mix-iny se zapisují podobně jako funkce (jak jinak, když JavaScript je funkcionální jazyk), kdy nejvnitřnější parametr je třída, od které dědíte a funkce jsou pak mix-iny, které se na ni aplikují.

Jak jsem psal v úvodu, ES6 nepřináší nové funkčnosti ale jen usnadňuje zápis stávajících. Mix-iny tedy fungují tak, že se vezme základní třídy (prototyp objektu) a do ní se překopírují (merge) vlastnosti mix-inu (mergovaný objekt). Znamená to tedy, že pokud třída a/nebo mix-iny obsahují stejné vlastnosti nebo funkce, přepíší se a zůstane platit ta z prvního mix-inu (zleva doprava dle zápisu extend). Je tedy potřeba dát pozor na to, aby mix-iny neměli stejně pojmenované vlastnosti a metody, pokud nechcete, aby se přepsali. Technicky je Mix-in funkce, která má za parametr prototyp třídy a přidá do něj nadefinované metody. Nemá tedy definovaný constructor ani prototype.

Pokud tedy do třídy Employee (zaměstnanec) přidáte mix-iny Children (děti) a Vacation (Dovolená), tak nemůžou oba definovat metodu getCount() (počet dětí a počet dní dovolené), protože pak by fungovala vždy jen jedna z nich. Můžete ale k mix-inu Children přidat mix-in DependentChildren, která metodu getCount() přepíše tak, že bude vracet jen počet nedospělých dětí (na které zaměstnanec dostává úlevy).

Důležité je ale uvědomit si, že zatímco super funguje na rodičovské třídy (které využívají prototypy), tak na mix-iny nefunguje, protože tam se metody přepisují. V příkladu z předchozího odstavce tedy nemůžete použít v metodě DependentChildren::getCount() konstrukci super.getCount(), protože „rodičovská“ metoda byla nadefinována v mix-inu Children a již k ní tedy nevede žádná reference. V nové metodě tedy budete muset zduplikovat kód původní metody pro výpočet všech dětí (a pak odečíst ty již dospělé).

Stejně tak na Mix-iny nefunguje metoda isInstanceOf().

Promise

Velký přínos do asynchronního zpracování JavaSkriptu přináší Promise (česky sliby) a nahrazují dříve používané callbacky a nepřehledné kódy, které vytvářejí:

//starý zápis s callbacky [ES5]
function doSomethingLong(callback) {
    var data = {};
    //...do something with data
    callback(data);
}
function doSomethingEvenLonger(data, callback) {
    //...do something with data
    callback(data);
}

doSomethingLong(function(response) {
    doSomethingEvenLonger(response,
            function(response) {
         console.log(response);
    });
});

Zápis s callbacky byl problematický v tom, že jste funkce buď nic nevrátila, nebo sice něco vrátila, ale vy jste nevěděli, jestli funkci callback zavolá nebo ne a nemohli jste další kód řídit podle toho.

Od ES6 může libovolná metoda vrátit Promise, nad kterým si volající metoda může zavolat then() nebo catch() a reagovat tak na úspěšné nebo neúspěšné dokončení. Metoda, která Promise vytváří, mu musí předat funkci, která provede vlastní výpočet (když se na to podáváte kriticky, zase tolik se to neliší od původního callbacku). Funkce má dva parametry: resolve a reject, což jsou funkce, kterým můžete předat buď výsledek, nebo chybu:

//nový zápis s promisy [ES6]
function doSomethingLong() {
    var data = {};
    return new Promise(resolve => (
        //...do something with data {
        resolve(data);
    );
}
function doSomethingEvenLonger(data) {
    return new Promise(resolve => (
        //...do something with data {
        resolve(data);
    );
}

//volání Promise pomocí funkcí
doSomethingLong()
    .then(function(response) {
        doSomethingEvenLonger(response)
        .then(function(response) {
            console.log(response);
        });
    })
;
//ten samý zápis se šipkovými funkcemi
doSomethingLong()
    .then(r => doSomethingEvenLonger(r))
    .then(r => console.log(r))
;
//Asynchonní 
function doSomething() {
    return new Promise((resolve, reject) => {
        let data = api.getData();
        if (data) {
            resolve(data);
        } else {
            reject(api.getLastError());
        }
    }
}

doSomething()
    .then((data) => ... zpracování dat)
    .catch((error) => ... zpracování chyby)
;

Async/Await [ES8]

Od ES8 můžete tenhle zápis převést na mnohem jednodušší pomocí klíčových slov async a await:

async function doSomethingLong() {
    var data = {};
    //...do something with data
    return data;
}
async function doSomethingEvenLonger(data) {
    //...do something with data
    return data;
}

var
    data = await doSomethingLong(),
    result = await doSomethingEventLonger(data)
;
console.log(result);

V novém kódu si všimněte, že se pořadí zápisu řádek a proměnných nijak neliší od zápisu, kde vše probíhá synchronně – funkce vrací hodnoty přes return, návratové hodnoty ukládáme do proměnných jednu za druhou atd.

Čím se liší, je právě použití slova async pro definici funkcí, čímž kompilátoru řekneme, že funkce nebude vracet hodnotu synchronně, ale že synchronně vrátí pouze Promise a hodnota bude vrácena asynchronně. Následně při zavolání funkce použijeme slovo await, čímž pozastavíme provádění skriptu do doby, než volaná funkce vrátí hodnotu a dokončí tak Promise.

Důležité je uvědomit si, že zatímco u asynchronního zápisu (callback, promise) po zavolání asynchronní funkce pokračovala dál hlavní funkce a teprve po skončení se zavoval callback nebo reakce na promise, tak u await se výkon hlavní funkce zastaví, dokud asynchronní funkce nehrátí hodnotu. V řeči vláken tedy await odpovídá metodě Thread.join(). Pokud tedy budete chtít přepsat starý asynchronní kód na nový a async a await, je potřeba si dát pozor na to, aby se funkce nevolali v jiném pořadí, než je potřeba pro správnou funkčnost!

Rozdíl oproti normálnímu synchronnímu volání je v tom, že zatímco se čeká na Promise (ať už starým zápisem nebo přes await), mohou se provádět ostatní činnosti (např. v prohlížeči se může renderovat DOM, mohou se provádět skripty zadané s async nebo type=module, atd.).

Pokud voláte několik vnořených asynchronních funkcí v sobě, je potřeba si uvědomit, že různým použitím await a async můžete dosáhnout různých posloupností spouštění:

async load(url) { ... } //stáhne data ze serveru

getSync(url) { //tahle funkce je synchronní
    return await load(url); 
}

async getAsynch(url) { //tahle funkce je asynchronní
    return await load(url);
}

async getPromise(url) { //funkce je asynchronní,
    return load(url);   //ale nečeká a vrací Promise
}

//volání
let 
    a = getSync('a.html'),
    b = getAsynch('b.html'),
    c = await getAsync('c.html');
    d = await getPromise('c.html');

b = b.then(b => b);
d = d.then(d => d);

Z uvedených funkcí se hodnoty a a c načtou synchronně (budou tedy rovnou obsahovat odpověď), zatímco b a d budou obsahovat Promise a odpověď se načte až v okamžiku zavolání then(), které jen překopíruje výsledek.

Pokud chcete čekat na více asynchronních volání, musíte použít metodu Promise.all():

async function doSomething() {
    //...do something
}
async function doSomethingElse() {
    //...do something different
}

await Promise.all(
          doSomething(),
          doSomethingElse()
);
console.log('All done');

Po použití await na Promise.all() dostanete jako návratovou hodnotu pole s výsledky jednotlivých funkcí. Můžete pak tedy použít destrukturalizaci pole (viz výše) pro jejich uložení.

Důležité je uvědomit si, že pokud async metodu zavoláte bez slova await, vrátí svůj Promise. Následně můžete použít await přímo na Promise a počkat na něj:

async function doSomething() { ... }

let promise = doSomething();
promise.then(r => console.log(r));
let result = await promise;
//uloží výsledek a zároveň ho zapíše do konzole

Finally [ES9]

Od verze 9 můžete kromě then and catch používat i finally, které se zavolá v obou případech (stejně jako u TRY-CATCH-FINALLY).

Metodu finally není potřeba povat na konci (tak jako u TRY-CATCH), ale je možné ji vložit kamkoliv mezi posloupnost then a catch. Prostě tak, jak je potřeba reagovat na úspěch, chybu nebo spouštět kód v obou případech, bez toho, abyste se museli opakovat:

fetch(url)
    .finally(r => console.log('Processing', r)
    .then(response => process(r))
    .finally(result => console.log('Result?', result)
    .catch(error => handle(error))
    .finally(() => console.log('Finished processing')
;

Čekání na Promisy [ES6/ES11]

Pokud máte více Promisů a potřebujete na ně počkat, můžete od ES6 použít metodu Promise.all(), která postupně počká na všechny předané Promisy:

let a = process1(); //asynchronní procesy
let b = process2();
let c = process3();

Process.all([a, b, c])
    .then((results) => ... vše uspělo)
    .catch((errors) => ... něco selhalo)
;

Nevýhoda Promise.all() je v tom, že vrátí chybu (zavolá catch) kdykoliv alespoň jeden z Promisů selže (resp. zavolá then jen pro ty Promisy, které uspěly před tím, než první selhal). Promise.all() tedy funguje v případě, že procesy jsou sice asynchronní, ale zpracování výsledku je synchronní (tedy následující process nemá smysl, pokud předchozí selhal).

Novější metoda Promise.allSettled() z ES11 provádí to samé s tím rozdílem, že vždy zavolá then a předá mu seznam výsledků, ať už jsou úspěšné (fulfilled) nebo ne (rejected):

let a = process1(); //asynchronní procesy
let b = process2();
let c = process3();

Process.allSettled([a, b, c])
    .then((results) => {
        if (results[0].status === 'fulfilled') {
            //... první proces uspěl
            result1 = results[0].value;
        }
        if (results[1].status === 'rejected') {
            //... druhý proces selhal
            error2 = results[1].value;
        }
    })
;

Nejrychlejší Promise [ES12]

Funkce Promise.all*() čekají na všechny Promisy a neskončí dříve, než doběhnout všechny zadané procesy. To se dá sice obejít tím, že ze všech Promisů zavoláte určitou metodu, která bude reagovat jen na ten první, ale není to ideální.

Pokud vás zajímá jen první doběhlý proces (např. načítáte data z různých zdrojů jako je cache, databáze nebo API a jde vám jen o tu nejrychlejší metodu, která má data), můžete použít funkce Promise.any() nebo Promise.race().

Funkce Promise.any() funguje tak, že vrátí první úspěšný výsledek (resolved) a ostatní (rejected nebo pozdější resolved) ignoruje. Pokud všechny procesy doběhnout, aniž by alespoň jeden z nich uspěl, vyvolá se výjimka AggregateError, kterou pak můžete odchytit pomocí catch().

Naproti tomu Promise.race() vrátí skutečně první výsledek ať už byl úspěšný nebo ne. To, jestli nejrychlejší proces uspěl nebo selhal pak poznáte díky metodám then() a catch(). Zde je potřeba si dát pozor, abyste nenapsali jeden z procesů tak, že selže hned na začátku, např. při kontrole vstupních parametrů – pak by totiž závod nedával smysl a vždy by vyhrál ten, který ve skutečnosti nemůže nic udělat (asi jako kdyby na Paralympiádě vždy vyhrál ten nejvíce postižený).

Promise.any([process1(), process2()])
    .then((result) => alert('Rychlejší proces skončil'))
    .catch(() => alert('Oba procesy selhaly'))
;

Promise.race([process1(), process2()])
    .then((result) => alert('Rychlejší process uspěl'))
    .catch((error) => alert('Rychlejší process selhal'))
;

Promise.any() se tedy hodí k získání dat z různých zdrojů, které mohou být v různých časech různě rychlé a některé z nich nemusí být dostupné a vy potřebujete ten nejrychlejší, který data vrátí. Například pokud máte několik API nebo kluster databází. Jen pozor na to, abyste zbytečně nepřetěžovali všechny zdroje najednou. Například pokud máte cache a databázy, tato metodu bude nejspíše nevhodná, protože nedává smysl ptát se databáze před tím, než ověříte, jestli máte data v cache.

Promise.race() je naopak vhodný v případě, že máte způsoby výpočtu nebo získání data, které jsou sice různě rychlé, ale vždy dostupné a selhání je součástí procesu. Dobrý příklad je třeba ověření platnosti emailové adresy (úspěch i selhání je tedy očekávaný výsledek), kvůli kterému musíte zavolat různá externí API, ale nevíte, které je zrovna nejrychlejší (a nezáleží vám na tom, které je nejlevnější). Stejně jako v předchozím případě je potřeba myslet na to, že některé zdroje mohou být levnější než jiné (ať už mluvíme o skutečných penězích nebo jen nákladech na provoz) a nemusí dávat smysl dotazovat všechny najednou (opět např. cache, která je víceméně zdarma, oproti placeným API).

Čísla

Při zadávání čísel můžete rovnou zadat binární nebo octalové (osmičkové) číslo:

//ES5
var binary = parseInt('0011', 2),
    octal  = 0123;
//strict mode
var binary = parseInt('0011', 2),
    octal  = parseInt('123', 8);
//ES6
var binary = 0b0011,
    octal  = 0o123;

BigInt

Od verze ES11 (někdo uvádí už ES10?) můžete při definici čísla na konec uvést písmeno „n“, čímž říkáte kompilátoru, že má uložit číslo jako 64-bitové (BigInt), díky čemuž dokáže uložit mnohem větší čísla než stávající 16 a 32 bitové implementace.

Pozor ale na to, že s BigInt umí pracovat jen nativní operátory (+, *, <, ==, **, atd.), zatímco funkce ze skupiny Math nemusí umět s BigInt pracovat a mohou vracet NaN; a to i v případě, že použijete malou hodnotu (např. „10n“).

Navíc nejde použít v jednom výpočtu normání a BigInt čísla a je potřeba všechny neznámé parametry převést pomocí BigInt(). A samozřejmě pozor na to že BigInt je pořád jen Integer, takže neumí pracovat s desetinámi čísly:

let five = 5;
let bigTwo = 2n;

bigTwo * five; // TypeError - nesedí typ čísel
bigTwo * BigInt(five); //== 10n
BigInt(five) / bigTwo; //== 2n
//dělení vrátí 2 místo 2.5, protože INT desetiny zahodí
8n / 9n //== 0n (0.888 je pořád nula)

let half = 2.5;
BigInt(half); // RangeError - nezpracuje desetiny
BigInt(Math.round(half)); //== 3n
BigInt(Math.floor(half)); //== 2n (očekáváno od BigInt)

Number(bigTwo); //== 2 (tedy zpět normální INT)

typeof bigTwo; //== 'bigint'

Naproti tomu operátory porovnání dokáží správně porovnat INT nebo FLOAT s BigInt:

1 === 1n // false (nesedí typ)
1 == 1n // true
1.0 == 1n // true
1 < 2n // true
5n > 4.999 // true
5n < 5.001 //true (překvapivě nepřetypuje na 5n < 5n)

0n && 1 // 0n (první nulová hodnota)
0n || 1 // 1 (první nenulová hodnota)

Oddělovač tisíců [ES12]

Od verze ES12 můžete zadávat velká čísla s oddělovači pro lepší čtení. Kompilátor bude podtržítka v číslech ignorovat.

let i = 123_456_789;
let japan_i = 1_2345_6789; 
let hexa = 0x12_34_56_FF;

(V japonštině se nepoužívají oddělovače tisíců, ale desetitisíců. Například milion není tisíc tisíců, ale doslova 百万 „sto desetitisíců“.)

Operátor druhé mocniny (power) [ES7]

Pro mocninu (tedy vynásobení čísla sama sebou) má ES funkci Math.pow(). Velká nevýhoda ES je ale právě v tom, že neelementární matematické operace se musí volat jako metody trídy Math.

Od ES7 tenhle problém řeší alespoň nový operátor mocniny, který se zapisuje jako dvojité násobení (tedy dvě hvězdičky) následované mocninou (tedy kolikrát se má číslo vynásobit):

let devet = 3**2; //== Math.pow(3, 2) == 3 * 3
let osm = 2**3; //== Math.pow(2, 3) == 2 * 2 * 2

Řetězce

ES6 vylepšuje podporu pro unicode znaky v regulárních výrazech a funkcích pro práci s řetězci.

Pro práci s řetězci přidává ES6 nové funkce:

Pad [ES8]

Funkce padStart() a padEnd() mohou prodloužit řetězec, pokud není dostatečně dlouhý. Snadno tak vyřešíte např. problém s úvodními nulami nebo minimálním počtem znaků:

Num.toBinary(5).padStart(16, '0'); //full word
password.padEnd(8, ' '); //minimálně 8 znaků

//práznou mezeru není potřeba uvádět:
password.padEnd(8); //přidá mezery

Regulární tečka pro všechno [ES9]

Když v regulárním výrazu použijete tečku, znamená to „jakýkoliv znak“… i když ne tak úplně. Tečka nikdy nebude pasovat na znak pro nový řádek (\n nebo \r) a má také problém s hledáním smajlíků (protože jeden znak je složen z několika bytů, takže musíte správně zadat počet).

Pokud za regulární výraz přidáte příznak /s (pojmenovaný DotAll flag), upravíte chování tak, že se ve vstupním řetězci nejrpve všechny unicode a escapované hodnoty převedou na skutečné znaky a pak teprve se na ně aplikuje regulární výraz. Díky tomu tečka pozná jak nový řádek, tak i vícebytové UNICODE znaky (aka smajlíci):

let value = 'Welcome \u2764\uFE0F,\n Here';
/Welcome .,. Here/.test(value); //vrátí False
//...protože tečka nenajde srdíčko ani nový řádek
/Welcome .,. Here/s.test(value); //vrátí True
//... protože s /s nemá problém
//... se smajlíkem ani odřádkováním

Pojmenované regulární indexy [ES9]

V regulárním výrazu můžete použitím závorky hledat konkrétní části, které se pak z metody exec vrátí jako indexy (capture groups).

Pokud ale za závorku uvedete otazník a jméno ve špičatých závorkách, bude hodnota kromě indexu vráceny i pod uvedeným jménem:

//Starý zápis
var r = /(.)(.)/.exec('Boobs'); // :)
alert(r[1] + r[2] + r[2]); //'Boo'

//ES9+ (teď už to není tak vtipné)
var r = /(?<left>.)(?<right>.)/.exec('Boobs');
alert(r.groups.left + r.groups.right); //'Bo'

Ořezání mezer [ES10]

Funkce String.trim() existuje v ES již delší dobu, ale umí ořezat jen všechny mezery. Místo ní ale můžete použít trimStart() nebo trimEnd(), podle toho, jestli chcete mezery ořezat jen na začátku nebo na konci.

str.trim();
str.trimStart();
str.trimEnd();

Snadnější nahrazení všech výskytů [ES12]

Metoda string.replace('from', 'to') nahradí jen první výskyt rětězce. Pokud chcete nahradit všechny výskyty, musíte použít regulární výraz s globálním modifikátorem: string.replace(/from/g, 'to').

V ES12 můžete místo toho použít metodu replaceAll, která funguje stejně, ale pouze s řetězci:

let str = ('abcbacad');
let from = 'a';
let to = 'x';

//starý zápis
str.replaceAll(new RegExp(from, 'g'), to);

//nový zápis
str.replaceAll(from, to); //== 'xbcbxcxd'

To je výhodné hlavně v případě, že řetězec, který nahrazujete je dynamický a uložený v proměnné a vy nechcete řešit, jak převést řetězec na regulární výraz.

Array

Pro práci s poli přináší ES6 (resp. novější verze) následující funkce:

includes() [ES7]

Když chcete zjistit, jestli pole obsahuje určitou hodnotu, doteď bylo potřeba použít metodu indexOf(), která ale vracela -1 místo false a nulu nebo větší číslo, pokud daná hodnota existuje. Nyní již můžete použít metodu, která přímo vrací True nebo False:

var arr = [1,2,3];

//starý zápis
if (-1 < arr.indexOf(5) { //... }

//nový zápis
if (arr.includes(5)) { //... }

Poznámka: tato metoda byla přidána ve verzi ECMAscript 2017 (7. verze), takže starší prohlížeče vydané v letech 2015 až 2017 ji nemusí obsahovat (i když podporují ostatní ES6 konstrukce).

Klíče a hodnoty [ES8]

Pro práci s vlastnostmi objektů bylo dříve potřeba používat FOR-IN (nebo FOR-OF od ES6). Často ale potřebujete jen získat seznam hodnot nebo klíčů objektů, které pak jen dál předáte (nemá tedy smysl používat cyklus pro jejich výpis.

Podobně, jako má PHP funkce array_keys() a array_values(), nabízí ES8 metody entries() a values() a také funkce Object.entries() a Object.values(), které vrátí seznam klíčů a hodnot pole (kde to u JS trouchu postrádá smysl) a hlavně i objektů.

Bohužel ale většina metod/funkcí nevrací jednoduché pole (tak jako v PHP), ale místo toho vrací buď iterátor nebo dvou-rozměrné pole (viz dále).

Jediný metoda, která má význam pro zjednodušení je Object.values(obj), které převede objekt na pole:

let obj = {a:1, b:2};
let arr = Object.values(obj); //vrátí pole: [1, 2]
let one = arr[0]; //== 1
let two = arr.length; //== 2

Detekce prázného objektu [ES8]

ES nemá přímo funkci, která by vám řekla, jestli objekt (např. data z JSON) obsahuje vůbec nějaké hodnoty a tak obvykle musíte použít cyklus:

let isEmpty = true;
for (let i of JSON.parse(response)) {
    isEmpty = false; //má alespoň jednu vlastnost
    break; //nepotřebujete procházet celou odpověď
}

Zneužitím výše uvedené nové funkce Object.values() můžete libovolný objekt převést na pole … a podívat je na jeho délku:

let isEmpty = !!Object.values(JSON.parse(res)).length;

Stejně samozřejmě můžete zjistit i počet vlastností, které jste z JSON nebo parametru funkce získali.

Flat (zploštění víceúrovňového pole) [ES10]

Pomocí metody Array.flat() můžete převést libovolné víceúrovňové pole na jednoduché pole. Pozor na to, že bez parametru funguje pouze na dvou-úrovňové pole. Pokud chcete převést libobolně hluboké pole, musíte jí předat parametr Infinity.

let a = [1, [2, [3, [4]]]];
let b = a.flat(); //== [1, 2, [3, [4]]]
let c = a.flat(2); //== [1, 2, 3, [4]]
let d = a.flat(Infinity); //== [1, 2, 3, 4]

Existuje ještě metoda flatMap, která funguje tak, že jako druhý parametr zadáte funkci, která dostane klíč a hodnotu každé položky, která se bude vkládat do výsledného pole.

Objekt z dvourozměrného pole [ES10]

Výše jsem uváděl, že funkce Object.entries nefunguje tak, jak byste očekávali, pokud znáte PHP funkci array_keys(). Je to proto, že Object.entries vrací dvourozměrné pole a naopak Object.forEntries umožňuje toto pole převést zpět na objekt.

let o = {a:1, b:2};
let a = Object.entries(o); //== [[a,1],[b,2]]
let q = Object.fromEntries(a); == {a:1, b:2}

To se může hodit například v případě, kdy komunikujete v jazykem, který neumí tak dobře zpracovávat objekty s neznámými vlastnostmi, ale nedělá mu problém zpracovat dvou-rozměrné pole.

At [ES14]

Tato funkce je definována pro verzi 14, která se má stát oficiální až v roce 2023, ale některé prohlížeče (konkrétně Safari 15.4) ji již definují dříve.

Funkce umožňuje číst pole odzadu pomocí negativních indexů a nahrazuje tak dnes používanou syntaxy Array[Array.length-1] pro získání posledního prvku pole.

let
    a = [1, 2, 3, 4, 5],
    b = a.at(0), //první prvek = 1
    c = a.at(1), //druhý prvek = 2
    d = a.at(-1), //poslední prvek = 5
    e = a.at(-2) //předposlední prvek = 4
;

Sady

ES6 přináší nový datový typ Set (česky sada). Sada funguje podobně jako pole s tím rozdílem, že každá hodnota v ní může být jen jednou. Odpadá tedy nutnost vytváření podobného chování pomocí pole:

//starý zápis s polem
var uniqueArray = [];
uniqueArray.pushIf = function(item) {
    if (!this.includes(item)) {
        this.push(item);
    }
};
uniqueArray.pushIf(1);
uniqueArray.pushIf(1);
console.log(uniqueArray.length); // 1

//nový zápis se sadou
var set = new Set();
set.add(1);
set.add(1);

console.log(set.size); //1

//Sada podporuje řetězení
console.log(set.add(2).add(3).size); //3

Hodnoty můžete ze sady odebírat, přičemž zároveň zjistíte, zda daná položka v sadě byla či nikoliv. Můžete také vymazat celou sadu:

var set = new Set();

if (set.delete(5)) {
    console.log('Položka odebrána');
}
else {
    console.log('Položka nenalezena');
}

set.add(1).add(2).clear();
console.log(set.size); // 0

Pro zjištění, zda sada obsahuje hodnotu, bez toho, abyste jí přidali nebo odebrali, slouží metoda has():

var set = new Set();

if (set.has(5)) {
    console.log('Položka je v sadě');
}
else {
    console.log('Položka nenalezena');
}

Symboly

Pro pojmenování vlastností objektů můžete použít speciální typ Symbol, který je určen k tomu, abyste od sebe mohli odlišit normální a speciální vlastnosti a nedocházelo ke kolizím. Vlastnosti pojmenované symbolem se totiž neukáží při použití iterátoru (FOR-IN, FOR-OF, apod.) a také se nevypíšou při konverzi do JSON. Naopak se ale zkopírují při použití metody Object.assign() a dalších podobných akcích (např. přidání mix-inu do třídy).

Anonymní symboly

Symbol se vytváří zavoláním metody Symbol() a každé takové zavolání vytvoří nový unikátní symbol, takže pro pozdější použití je potřeba si ho uložit do proměnné. Funkci sice můžete předat parametr, ale ten slouží pouze jako popis k výpisu do konzole a to, že dva symboly mají stejný popis neznamená, že jsou stejné.

var password = Symbol('password'),
    user = {
        name: 'john',
        [password]: '123456'
    }
;
console.log(JSON.stringify(user));
    // vypíše '{name: "john"}'
console.log(user[password]);
    //vypíše '123456'
console.log(password);
    //vypíše 'Symbol(password)'

Pro získání všech vlastností uložených pod symbolem můžete použít metodu Object.getOwnPropertySymbols(), která vrací pole symbolů použitých v daném objektu:

//navazuje na předchozí příklad...
var symbols = Object.getOwnPropertySymbols(user);
for (let symbol of symbols) {
    console.log(symbol, user[symbol]);
} //vypíše 'Symbol(password)' '123456'

Globální symboly

Abyste si nemuseli vlastní symboly ukládat do proměnných, můžete použít globální registr, který se ukrývá v metodě Symbol.for() (česky ‚symbol pro něco‚). Pokud zavoláte metodu se jménem symbolu, metoda ho vytvoří a následně vždy vrátí ten samý. Je potřeba ale dát pozor, že tento registr je skutečně globální a je sdílen i se skripty v iFramech, Service Workerech, knihovnách (modulech) atd. Je tedy vhodné pojmenovávat symboly podle toho, kde jsou použity (např. „lib.utils.helperClass.errorState“) nebo stylem Java balíků (např. „com.company.lib.errorState“)

var user = {
        name: 'john',
        [Symbol.for('password')]: '123456'
   }
;
console.log(JSON.stringify(user));
    // vypíše '{name: "john"}'
console.log(user[Symbol.for('password')]);
    //vypíše '123456'

pass = Symbol.for('password');
console.log(Symbol.keyFor(pass));
    //vypíše 'password', tedy klíč symbolu
conso.e.log(Symbol.keyFor(Symbol('xxx')));
    //vypíše undefined, protože xxx není
    //registrován jako globální symbol!

Práce se symboly

Důležitá věc je, že symboly jdou z objektu přečíst stejně jako jeho (veřejné) vlastnosti. Lze je sice využít pro ukládání privátních vlastností, takže se nevypíší do JSON ani v cyklech, ale stále k nim kdokoliv může získat přístup, pokud zná jejich jméno, nebo prostě použije Object.getOwnPropertySymbols().

Kromě výpisu do konzole a převedení metodou toString(), jakákoliv jiná akce se symbolem skončí chybou:

Symbol() + 1; //error
'Unique: ' + Symbol(); //error
'Unique: ' + Symbol().toString();
    // vytvoří 'Unique: Symbol()'

Od ES10 můžete místo toString() použít descriptiom, které vrátí původní popis bez přidaného Symbol():

alert(Symbol('Muj symbol').description); //"Muj symbol"

Symboly mohou složit jako konstanty pro speciální případy tam, kde není vhodné použít čísla nebo řetězce (které se dají snadno zapsat jako magická hodnota):

const DEBUG = 'debug';
log(DEBUG, 'info');
log('debug', 'info'); //chybný zápis
   //ale funkce log() to nepozná

const DEBUG_SYMBOL = Symbol.for('LOG_DEBUG');
log(DEBUG_SYMBOL, 'info');
log('LOG_DEBUG', 'info');
    //funkce log() pozná chybný parametr a
    //může vyhodit Error

Speciální symboly

ECMAscript 6 sám definuje některé symboly, které lze použít (nebo je nutno použít) pro vytvoření nějaké speciální funkcionality (podobně jako má PHP magické metody začínající ‚__‚. Tyto symboly jsou uloženy v namespace Symbol.

hasInstance

Symbol Symbol.hasInstance lze použít k vytvoření metody, která bude zavolána, když na objekt použijete operátor instanceof. Objekt se tak může vydávat za potomka nějaké třídy nebo objektu, i když to není pravda a jen simuluje jeho chování. Pamatujte, že v případě třídy je potřeba metodu vytvořit jako statickou!

Např. pokud vytvoříte objekt, který se chová jako seznam uživatelů, takže by mohl být potomkem Array ale není:

class UserList {
    constructor() { this.list = []; }
    get length() { return this.list.length; }
    push(user) { this.list.push(user); }
    static [Symbol.hasInstance](parent) {
        return (parent instanceof Array);
    }
}

var ul = new UserList();
console.log(ul instanceof Array); //true
console.log(ul instanceof User); //false

Iterátor

Další Symbol.iterator slouží k napodobení chování pole, kdy můžete libovolný objekt upravit tak, aby ho bylo možno procházet cyklem stejně jako pole. Metoda uložená pod tímto symbolem musí být nadefinována jako generátor:

class UserList {
    constructor() { this.list = []; }
    get length() { return this.list.length; }
    add(user) { this.list.push(user); }
    static [Symbol.hasInstance](parent) {
        return (parent instanceof Array);
    }
    *[Symbol.iterator]() {
        for (let i in this.list) {
            yield this.list[i];
        }
    }
}

var ul = new UserList();
for (let user of ul) {
    console.log(user);
}

Práce s řetězci

Pro objekty, které obsahují řetězcovou hodnotu, můžete použít symboly match, replace a search, abyste umožnili použití objektu v příslušných metodách:

class MyValue {
    constructor(value) { this.value = value; }
    [Symbol.match](str) {
        return this.value === str;
    }
    [Symbol.replace](str, repl) {
        str.replace(this.value, repl);
    }
    [Symbol.search](str) {
        return str.indexOf(this.value);
    }
}
var val = new MyValue('foo');
console.log('foo'.match(val)); //true
console.log(
    'foobar'.replace(val, 'xxx')); //xxxbar
console.log('xxxfoo'.search(val)); // 3

Primitive

Pro převod vašeho objektu na primitivní (základní) typ můžete použít Symbol.toPrimitive. Daná funkce bude zavolána kdykoliv, kdy bude daný objekt použit v nějaké operaci a jako vstupní parametr dostane jméno typu, na který by se měl převést (funguje tedy podobně jako funkce __toString() v PHP):

class MyValue {
    constructor(value) { this.value = value; }
    [Symbol.toPrimitive](type) {
        if ('string' === type) {
            return 'Value('+this.value+')';
        }
        if ('number' === type) {
            return parseInt(this.value);
        }
        return undefined;
    }
}
var val = new MyValue('15');
console.log(10 + val); //25
console.log('' + val)); //'Value(15)'

Null-conceiling a default operátory [ES11/ES12]

Tyto operátory jsou známy např. z C# 8 a PHP 7 a umožňují nastavit výchozí hodnotu proměnné (nebo parametru funkce), pokud očekávaná proměnná (nebo vstupní parametr) obsahuje nedefinovanou hodnotu.

Zatímco ve většině jazyků funguje operátor skutečně jako Null-conceiling (tedy reaguje jen na hodnotu NULL), v případě ECMAscriptu (resp. Javascriptu) reaguje i na hodnotu undefined.

let add = function(a, b) {
    a ??= 0; // pokud není parametr zadán, použij 0
    b ??= 0;

    return a + b;
}

c = add(a ?? 0, b ?? 0); //pokud nejsou definovány
                                      //hodnoty a a b, použij 0

d = null ?? undefined ?? void 0 ?? 'unknown';
  //d == 'unknown', protože žádná z předchozích
  // hodnot není platná pro null-conceiling operátor

Operátor „??“ byl přidán ve verzi 11, zatímco „??=“ je použijtelný až od verze 12.

Na rozdíl od dříve používaného OR operátoru („||“) reaguje null-conceiling pouze na NULL a undefined, zatímco OR operátor reagovat i na hodnoty False a NaN:

a = null ?? undefined ?? false ?? 0/0 ?? 'unknown'; //== false
b = null || undefined || false || 0/0 || 'unknown'; //== 'unknown'

Kromě default operátoru přidává verze 12 i dříve neexistující operátory pro zkrácený logický neúplný AND a OR, které fungují obdobně jako Null-conceiling ale s hodnotami True a False:

a = process1();
a &&= process2();
//odpovídá:
a = (process1() && process2());
//tedy process2 zavolá jen pokud process1 vrátí true

b = process1();
b ||= process(2);
//odpovídá
b = process1() || process2();
//tedy process2 zavolá jen pokud process1 vrátí false

Vylepšení Garbage collectoru [ES12]

Garbage collector (GC) se v ES stará o odstranění nepotřebných dat z paměti. Základní pravidlo je, že pokud některá proměnná nebo vlastnost odkazuje na data, nemůže je GC odstranit.

ES12 přináší dvě možnosti, jak toto obejít:

Zaprvé nový kontruktor WeakRef() umožňuje vytvořit nebo uložit objekt bez toho, aby se na něj vytvořila pevná reference, která by bránila GC v jeho odstranění. Před použitím objektu z WeakRef je tedy vždy potřeba ověřit, že stále existuje. Hodnota vytvořená kontruktorem WeakRef() nejde použít přímo a vždy je potřeba získat tvrdou reference na původní objekt – nebo undefined, pokud již neexistuje:

let ref = new WeakRef(element); //měkká reference
//...
let el = ref.deref(); //získání tvrdé reference
if (el) {
     process(el);
}
else {
    alert('Element by odstraněn z paměti');
}

Alternativy k WeakRef() jsou WeakMap() (pole) a WeakSet() (sada). Zatímco do pole ukládáte objekty podle klíče a můžete je podle něj i získat (pokud ještě existuje), tak sada slouží k ověření, zda jste již s objektem pracovali (a na rozdíl od Set nebrání GC v uvolnění paměti).

let els = new WeakMap();

els.set('first', el1).set('second', el2);
//...
if (els.has('first') && els.get('first')) {
    process('els.get('first'));
    els.delete('first');
}
else {
    alert('First už neexistuje');
}

Zajímavé je, že u WeakMap je jedno, jestli objekt vložíte do první nebo druhého parametru set(). Jediné pravidlo je, že první hodnota slouží jako klíč a druhá jako návratová hodnota. Pokud tedy vložíte různé objekty do prvního i druhého parametru, oba budou uloženy měkkou referencí. Druhá hodnota může být i undefined a pak se pole chová víceméně jako sada:

let els = new WeakSet();

let startProcess = (el) {
    if (els.has(el)) {
        return false; //already started
    }
    els.add(el);
    process(el); //asynchronní proces?
}
let stopProcess(el) {
    els.delete(el);
}

WeakSet je vhodná například při rekurzi nebo Promisech (pro jednoduchost jsem to v příkladu uvedl jen obecnou metodu process()), kdy do ní uložíte všechny aktuálně zpracovávané elementy a kontrolujete, zda nezkoušíte zpracovat stejný objekt dvakrát (kruhová reference nebo jen plýtvání zdroji). Pokud byste pro tohle použili obyčejný Set nebo Array, nebylo by možno objekty uvolnit z pamětiv případě, že by proces trval příliš dlouho.

Pozor na to, že WeakRef, WeakMap a WeakSet je možno použít jen v případě, že vám skutečně nezáleží na tom, zda objekt stále existuje nebo ne. Vhodný způsob použítí je například prvky DOMu, které můžete vždy znovu získat:

let els = new WeakMap();

//Cache pro JS wrappery DOM prvků
let getEl = (id) => {
    el = els.get(id);
    if (el) {
        return el;
    }
    el = document.getElementById(id);
    els.set(id, el);
    return el;
}

Také pozor na to, že mapa má metodu set() (nastavuje hodnotu pro klíč), zatímco WeakSet má metodu add() (přidává do sady)!

Druhé vylepšení GC je nová třída FinalizationRegistry(), která vyvolá vaši funkci, když se GC chystá registrovaný objekt zničit:

let onDestroy = new FinalizationRegistry((value) => {
    alert('Prvek '+value+' bude uvolněn z paměti);
}

let getEl = (id) => {
    let el = document.getElementById(id);
    onDestroy.register(el, id);
    return el;
}

Tento příklad vypíše id každého prvku, který jde získali z DOM, v okamžiku, kdy bude uvolněn z paměti a bude ho potřeba získat znovu. Hodí se jak pro debugování memory leaků, tak při práci s WeakRef().

1 komentář u „ES6 v kostce“

Napsat komentář

Vaše e-mailová adresa nebude zveřejněna. Vyžadované informace jsou označeny *

Tato stránka používá Akismet k omezení spamu. Podívejte se, jak vaše data z komentářů zpracováváme..