Generators, yield e iterators
Continuando com nossa série sobre o uso de JavaScript de forma assíncrona, não podemos deixar de passar pelos Generators.
É muito comum precisarmos manipular itens de uma lista e, para tal, o JavaScript evoluiu e passou a nos oferecer diversos métodos como map e filter em Arrays, e construtores na própria linguagem como for...of e for...in.
Justamente com esta evolução toda, ES6 nos trouxe os generators.
Conceito
Generators seguem um conceito onde você pode "continuar" de onde parou anteriormente, até completar uma determinada tarefa.
Por isso, acostume-se com a ideia de interromper o fluxo de sua função antes de terminar a tarefa, mas lembrando que a "situação" atual da função será retomada na próxima execução para continuar de onde havia parado.
Para auxiliar, temos os iterators symbol, o iterator, a spark e o novo token yield. Todos descritos abaixo :)
Iterator Symbol
Uma novidade no ES6 foi, justamente, a adição de Symbols. Eles nos oferecem acesso a funcionalidades da linguagem em um nível nunca antes visto.
O símbolo iterador é representado por @@iterator
e pode ser acessado por meio da constante Symbol.iterator
.
O mais legal é que podemos usá-lo até mesmo em nossos objetos nativos:
let arr = [1, 2, 3, 4, 5, 6, 7, 8, 9]; let it = arr[Symbol.iterator](); console.log(it.next().value); // 1 console.log(it.next().value); // 2 console.log(it.next().value); // 3 console.log(it.next().value); // 4
Ao chamarmos arr[Symbol.iterator]()
, recebemos o iterador dessa nossa Array. Hora de nos aprofundarmos mais nisso...
Iterator
Um iterator (iterador) é um objeto que oferece a funcionalidade de navegar entre os itens de uma lista ou estrutura - um a um - em sequência. Este objeto oferece o método next
, o qual retornará o próximo item da lista no seguinte formato:
{ value: Mixed, done: Boolean }
Onde value
corresponde ao valor do item atual, e done
, é um booleano que será true
quando não houverem mais itens a serem iterados. Isso significa que, quando done
for true
, o value em value
será undefined
. Usaremos os generators a seguir para construirmos uns exemplos úteis com iterators.
Spark *
Para tornarmos uma função no JavaScript em um generator, usamos o token *
junto ao token function
.
function* myGen () { // ... }
O nome deste
*
(asterisco), que em inglês seria chamado destar
é, na verdade, chamado despark
("fagulha" ou "faísca").
Isso quer dizer que - toda vez que executarmos tal função - ela nos retornará, na realidade, um novo generator.
yield
Acrescentamos um novo token, o yield. Yield remete-se à colheita no campo. Imagine que você precise fazer a colheita, mas é impossível finalizar toda a tarefa de uma só vez, você precisará voltar no dia seguinte para continuar de onde parou.
function* gen() { yield "A"; yield "B"; yield "C"; } var g = gen(); // "Generator { }"
E assim, podemos usar nosso generator como um iterator.
console.log(gen.next().value); // A console.log(g.next().value); // B console.log(g.next().value); // C console.log(g.next().value); // undefined console.log(g.next().done); // true
O interessante é que iterables seguem o mesmo padrão, portanto, se usarmos o Symbol.iterator podemos ainda ir mais longe:
var myList = {}; // aqui, definimos COMO nosso objeto itera myList[Symbol.iterator] = function* () { yield "A"; yield "B"; yield "C"; }; for(item of myList){ console.log(item); } // ["A", "B", "C"]
Outro uso seria "explodindo" a nossa lista: [...myList]; // ["A", "B", "C"]
Podemos também usar o yield
para gerarmos "infinitos" valores dinamicamente utilizando um "loop eterno": // nossa constante será o próprio generator const IDGenerator = (function* (){ var i =1; while (true) { yield i++; } })(); // e agora teremos quantas IDs únicas precisarmos IDGenerator.next().value; // 1 IDGenerator.next().value; // 2 IDGenerator.next().value; // 3 IDGenerator.next().value; // 4 IDGenerator.next().value; // 5
Notem que este iterador nunca será concluído, done
, nunca será true
. Yield* Temos também como usar o yield*
para nos referenciarmos a outro generator. Neste caso, este yield só será "resolvido" quando o generator apontado por ele estiver em done: true
, ou seja, quando ele tiver passado por todos os seus passos. function* subTopics() { yield "B1"; yield "B2"; yield "B3"; } function* topics() { yield "A"; yield "B"; yield* subTopics(); yield "C"; } var iterator = topics(); console.log(iterator.next()); // { value: "A", done: false } console.log(iterator.next()); // { value: "B", done: false } console.log(iterator.next()); // { value: "B1", done: false } console.log(iterator.next()); // { value: "B2", done: false } console.log(iterator.next()); // { value: "B3", done: false } console.log(iterator.next()); // { value: "C", done: false } console.log(iterator.next()); // { value: undefined, done: true }
O "Q" da questão aqui é que podemos utilizar o *yield** para generators, mas também para objetos iteráveis! function* topics() { yield* [1, 2, 3]; } var iterator = topics(); console.log(iterator.next()); // { value: 1, done: false } console.log(iterator.next()); // { value: 2, done: false } console.log(iterator.next()); // { value: 3, done: false } console.log(iterator.next()); // { value: undefined, done: true }
Ou até mesmo em strings: function* topics() { yield* "felipe"; } var iterator = topics(); console.log(iterator.next()); // { value: "f", done: false } console.log(iterator.next()); // { value: "e", done: false } console.log(iterator.next()); // { value: "l", done: false } console.log(iterator.next()); // { value: "i", done: false } console.log(iterator.next()); // { value: "p", done: false } console.log(iterator.next()); // { value: "e", done: false } console.log(iterator.next()); // { value: undefined, done: true }
Concluindo Enfim, como podem ver, trata-se de uma funcionalidade muito útil e que pode ser utilizada para "N" diferentes fins. Caso queira se inteirar mais no assunto, separamos alguns links: Iterable Protocol Generators na MDN Function* Yield Yield* Por que não aproveita e deixa nos comentários uns testes e usos diferentes para generators e iterators?