Generátor dat (ECMA6 function* a yield)

Nová verze JavaScriptu (známá jako ECMAscript 6, ES6 nebo ECMAScript 2015) přináší nová klíčová slova function* a yield, které můžete použít pro tzv. Generátory.

Smysl generátoru je v tom, že funkce může při každém zavolání vrátit jinou hodnotu aniž by bylo potřeba si nějak pamatovat, jaká byla poslední vrácená hodnota. Funkce zpravidla vypadá tak, že uvnitř cyklu vracíte hodnotu pomocí yield a JS sám zajistí, že funkce bude fungovat jako generátor.

Použití generátoru

Generátor se definuje stejně jako obyčejná funkce, jen s použitím jiných klíčových slov:

var generator = function*() {
    for (var i = 1; i < 10; i++) {
        yield i;
    }
}

Generátor definujete klíčovým slovem function* a používá se stejně jako původní function. Uvnitř funkce pak místo return používáte yield s tím rozdílem, že yield můžete použít opakovaně (v cyklu nebo i v kódu) a JavaScript při každém zavolání bude pokračovat od posledního použití yield.

Uvedený generátor lze použít takto:

var values = generator();
var i;
do {
    i = values.next();
    console.log(i.value);
} while (!i.done);

Nejprve je potřeba zavoláním funkce generátoru získat objekt Generator. Ten má metodu next(), pomocí které pak postupně voláte kód funkce, který se automaticky zastaví pokaždé, když najde klíčové slovo yield. Dalším zavoláním metody next() pak funkce pokračuje.

Metoda next() vrací objekt, který má dvě vlastnosti: value a done. Vlastnost value obsahuje hodnotu vrácenou pomocí yield a hodnota done je false. V okamžiku, kdy kód generátoru dojde na konec (nebo k return), bude návratový objekt funkce next() obsahovat ve value hodnotu undefined a done bude true.

Na konec můžete generátor přesunout i pomocí funkcí return() nebo throw(chyba):

do {
    i = values.next();
    if (i.value > 10) {
        i.throw('Chybná hodnota');
    } else if (i.value > 5) {
        i.return(); //hodnoty 6 - 9 nevrátí
    }
    console.log(i.value);
} while (!i.done);

Generátor jako Factory

V praxi se Generátor dá použít jako Factory, což znamená, že generátoru předáte hodnotu a on na jejím základě postupně počítá výsledky.

Nejjednodušší příklad je postupný výpočet mocniny:

//původní JS funkce je Math.pow()
Math.power = function*(base) {
    var value = 1;
    while (true) { //nekonečný generátor
        yield value *= base;
    }
}

var binary = Math.power(2);
var octal = Math.power(8);
var hexadecimal = Math.power(16);

Použití takových generátorů je pak otázka volání jejich metody next() a případně return():

//hledá nejbližší větší mocninu dvou
var binary = Math.power(2);
var search = $('#searchInput').val();
var power, found;

do {
    power = binary.next();
    if (power.value > search) { 
        found = power.value;
        binary.return(); 
    }
} while (!power.done);
alert('K čísli ' + search + ' je nejbližší'
 + ' menší mocnina dvou hodnota ' + found);

Funkce postupně prochází mocniny dvou (2, 4, 8, 16, 32, atd.) a zjišťuje, jestli je hodnota stále menší než uživatelem zadaná hodnota. Pokud je generátorem vrácená hodnota větší, zavolá se jeho metoda return(), čímž se generátor ukončí, na což zareaguje do/while cyklus a také skončí. Následně se zobrazí poslední získaná hodnota.

Například pro hodnotu 1000 najde hodnotu 1024, protože to je nejbližší větší mocnina dvou.

Generátor pro zpracování pole

Další způsob použití je zpracování dat z pole, kdy potřebujete postupně zpracovávat hodnoty.

function* getBrowserLanguage(languages) {
  var i, cnt = languages.length;
  for (i = 0; i < cnt; i++) {
    lang = languages[i];
    if (-1 < lang.indexOf('-')) {
      lang = lang.split('-');
      yield 'Prohlížeč podporuje jazyk '
       + lang[0] + ' pro stát ' + lang[1];
    }
    else {
      yield 'Prohlížeč podporuje jazyk '
           + lang;
    }
  }
}
langs = navigator.languages;
langs = getBrowserLanguage(langs);

Postupným voláním langs.next() pak získáte seznam všech jazyků, které váš prohlížeč podporuje.

Lepší cyklus

Abyste nemuseli složitě ošetřovat výsledek generátoru a zabývat se hodnotami value a done, obsahuje nový ECMAScript i nový zápis cyklu for, který s generátorem umí pracovat:

//hledá nejbližší větší mocninu dvou
var binary = Math.power(2);
var search = $('#searchInput').val();
var power, found;

for (power of binary) {
  if (power > search) {
    found = power;
    break;
  }
}

alert('K čísli ' + search + ' je nejbližší'
 + ' menší mocnina dvou hodnota ' + found);

Operátor of funguje podobně jako in s tím rozdílem, že sám ošetřuje stav hodnoty done a ukončí cyklus. Také do uvedené proměnné neukládá celý výsledek generátoru (objekt) ale jen jeho hodnotu value.

Zpětná kompatibilita

Jelikož jde o novou funkčnost, je potřeba nějak zajistit, že i starší prohlížeče (nebo ty, co ji zatím nepodporují) budou schopny funkci používat.

Ověření, že jsou generátory podporovány, provede pomocí eval funkce a try/catch:

window.yieldSupport=(function(test,method){ 
    try { return window[method](test); } 
    catch(e) { return false; } 
})("(function(test){test=function*(){'
    + 'yield true;};return test()'
    + '.next().value;})()", 'ev' + 'al');

Následně, pomocí uložené proměnné yieldSupport, musíte rozhodnout, jestli můžete použít nový způsob, nebo musíte použít nějakou náhražku (polyfill).

if (yieldSupport) {
    //CHYBA!!!
    Math.power = function*() { ... };
} else {
    Math.power = function() { ... };
}

Nemůžete ale použít tento jednoduchý způsob, protože použití function* ve starém JavaScriptu způsobí chybu. Musíte tedy zajistit, že nový kód se ke starému JavaScriptu vůbec nedostane.

To můžete udělat pomocí eval:

if (yieldSupport) {
    //dobře, ale ne moc přehledné
    var code = "(function*() { ... })";
    Math.power = eval(code);
} else {
    Math.power = function() { ... };
}

Eval zajistí, že starý JavaScript ho nezpracuje a tudíž mu nedojde, že obsahuje neznámá klíčová slova – chápe ho totiž jako obecný string a nezajímá ho obsah.

Použití eval ale není moc šikovné, protože ve stringu nemůžete používat výhody vývojářského editoru (např. dokončování kódu, validace, apod.). Proto je lepší použít dynamické stahování JavaScript souborů a verze pro nový a starý JS uložit do samostatných souborů:

//metoda 1 (v inline skriptu)
//vloží script podle verze JS
document.write('<scr'+'ipt src="' + (yieldSupport ? '/js/func_es6.js' : '/js/func_es5.js' ) + '"></scr'+'ipt>'
); 

//metoda 2 (dynamické vytvoření)
var s = document.createElement('script');
s.src = (yieldSupport
        ? '/js/func_es6.js'
        : '/js/func_es5.js'
);
document.body.appendChild(s);

//metoda 3 (AJAX)
$.getScript(yieldSupport
        ? '/js/func_es6.js'
        : '/js/func_es5.js'
);

Tím zajistíte, že nový kód se dostane pouze k prohlížečům podporující ES6 a staré prohlížeče se nebudou snažit zpracovat něco, čemu nerozumí. Navíc, díky tomu, že kódy jsou odděleny v samostatných souborech, prohlížeče podporující nový kód nemusí zbytečně stahovat ten starý a naopak (což v případě výše uvedeného eval() musí).

Pro uvedený generátor mocnin by polyfill pro ES5 vypadal takhle:

Math.power = function(base) {
   lastValue = 1;
   return {
    next: function() {
        if (lastValue < 0) {
            return {done:true};
        }
        return {
            value: lastValue *= base,
            done: false
        };
    },
    return: function() { lastValue = -1; },
    throw: function(error) {
        lastValue = -1;
        throw error;
}

Ještě na konec jeden způsob, který rovnou kombinuje ověření a definici a lze použít v triviálních situacích:

try { //zkus kód pro ES6
    var code = "(function*() { ... })";
    Math.power = eval(code);
} catch(e) { //pokud selže, použit polyfill
    Math.power = function() { ... };
}

Podpora

V současné době (podzim 2015) podporují generátory prohlížeče s JavaScript jádry od Mozilly (Firefox) a Googlu (Chrome, Opera, Android). Naopak prohlížeče od Microsoftu (IE, Edge) a Applu (Safari) je zatím neumí.

Napsat komentář

Vaše emailová 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..