Iterators e Generators no JavaScript

O que são Iterators?

Um iterator no JavaScript é um objeto que implementa o protocolo de iteração. Esse protocolo nada mais é do que um método em um dado objeto JavaScript que vai definir como esse objeto pode ser percorrido. Esse método deve retornar sempre um objeto com um método next, o qual retorna um outro objeto contendo a chave done que será um boleano indicando se a iteração chegou ao fim ou não, e a chave value contendo o valor do item atual na iteração.

Muito complicado até aqui? Continue a leitura e veremos com mais detalhes abaixo.

Como identificar um objeto iterável?

Nos objetos, o método que implementa o protocolo de iteração é representado por uma chave especial retornada por Symbol.iterator (Symbol é um novo tipo no JavaScript).

Os objetos que implementam esse método no JavaScript podem ser percorridos usando a instrução for...of, utilizando o operator ... nos Arrays ([...<iterable>]) ou sendo convertidos para Array usando Array.from(<iterable>).

Objetos iteráveis nativos no JavaScript

Alguns objetos no JavaScript por padrão já implementam esse protocolo, como por exemplo:

  • String
  • Array
  • Map
  • Set
  • arguments (dentro de funções)
  • NodeList (no browser)

Podemos verificar isso da seguinte forma:

const str = 'my string';
const arr = [1, 2, 3];
const divs = document.querySelectorAll('div');
const fn = function fn() {
return typeof arguments[Symbol.iterator] === 'function';
}
typeof str[Symbol.iterator] === 'function'; // true
typeof arr[Symbol.iterator] === 'function'; // true
typeof divs[Symbol.iterator] === 'function'; // true
fn() // true

Sabendo disso conseguimos entender porque as seguintes operações funcionam:

[...'my string'] 
// ["m", "y", " ", "s", "t", "r", "i", "n", "g"]
Array.from('my string') 
// ["m", "y", " ", "s", "t", "r", "i", "n", "g"]
const sum = function fn(a, b) {
return [...arguments].reduce((a, b) => a + b);
}
sum(10, 20) 
// 30
for (let v of 'my string') {
console.log(v);
// 'm'
// 'y'
// ' '
// 's'
// 't'
// 'r'
// 'i'
// 'n'
// 'g'
}
for(let v of [10, 40, 50]) {
console.log(v);
// 10
// 40
// 50
}

Implementando um objeto Iterável

Revendo o que foi descrito no início do artigo, para ser iterável o objeto deve:

  • Ter um método [Symbol.iterator]
  • O método deve retornar um objeto com a chave { next: Function }
  • next deve ser uma função que retorna um objeto contendo as chaves done e value.

Para simplificar, vejamos o exemplo abaixo:

const myIterable = {
[Symbol.iterator]: function() {
let limit = 5;
let i = 0;
return {
next: function() {
return {
done: i >= limit,
value: i < limit ? i++ : undefined
}
}
}
}
}

Observe acima que o método iterador armazena o estado.

Agora conseguimos percorrer nosso objeto usando por exemplo:

for(v of myIterable) {
console.log(v);
// 0
// 1
// 2
// 3
// 4
}
[...myIterable] // [0, 1, 2, 3, 4]

Ou diretamente acessando uma instância do iterador

const iter = myIterable[Symbol.iterator]();
iter.next() // {done: false, value: 0}
iter.next() // {done: false, value: 1}
iter.next() // {done: false, value: 2}
iter.next() // {done: false, value: 3}
iter.next() // {done: false, value: 4}
iter.next() // {done: true, value: undefined}

Todo esse processo para definir um Iterator é bem verboso, e existe uma forma mais simples de se fazer o mesmo, que é usando um outro recurso novo no JavaScript que são os Generators, como veremos a seguir.

O que são Generators?

No JavaScript, Generators são um tipo especial de função definidos pela seguinte sintaxe:

function* myGenerator() {
yield <value>
}

Vale observar que não é possível definir um generator utilizando a sintaxe de arrow function, pois o JavaScript só reconhece que uma função define um generator através da declaração function*.

Qual a relação entre Generators e Iterators?

Um generator ao ser executado retorna um Iterator com comportamentos idênticos aos explicados acima.

Vamos analisar um exemplo:

function* myGenerator() {
yield 10;
yield 40;
yield 50;
}
const myIterator = myGenerator();
myIterator.next(); // {value: 10, done: false}
myIterator.next(); // {value: 40, done: false}
myIterator.next(); // {value: 50, done: false}
myIterator.next(); // {value: undefined, done: true}

A palavra-chave yield vai definir o valor retornado a cada chamada do objeto iterador.

O mesmo comportamento ocorre ao passar para um Array usando o operador ...

function* myGenerator() {
yield 10;
yield 40;
yield 50;
}
const myIterator = myGenerator();
[...myIterator] // [10, 40, 50];

Note que a execução das chamadas de yield são lazy, ou seja elas não são computadas todas de uma vez. A cada execução do iterador o estado da função permanece salvo e pode ser acessado nas execuções posteriores. Essa funcionalidade abre um leque de possibilidades na linguagem.

Podemos por exemplo criar gerador de números infinitos:

function* makeId() {
let n = 1;
while(true) {
yield n++;
}
}
const id = makeId();
id.next(); //  {value: 1, done: false}
id.next(); // {value: 2, done: false}
id.next(); // {value: 3, done: false}
id.next(); // {value: 4, done: false}
id.next(); // {value: 5, done: false}
... // infinitamente

O que você acha que acontece ao executar o código abaixo no seu browser?

function* makeId() {
let n = 1;
while(true) {
yield n++;
}
}
const id = makeId();
[...id]

Provavelmente seu browser travou, na tentativa de criar um Array com infinitos itens.

A declaração yield*

Um recurso interessante de yield é que é possível delegar a execução para outro iterator.

Veja no exemplo abaixo:

function* generatorA() {
yield 1;
yield 2;
}
function* generatorB() {
yield 40;
yield* generatorA();
yield 20;
}
const iteratorB = generatorB();
iteratorB.next() // 40
iteratorB.next() // 1
iteratorB.next() // 2
iteratorB.next() // 20

Podemos passar para yield* qualquer objeto que implemente o protocolo de iteração.

function* generatorA() {
yield* ['h', 'i'];
yield 'foo';
yield* 'bar';
}
iteratorA = generatorA();
iteratorA.next() // 'h'
iteratorA.next() // 'i'
iteratorA.next() // 'foo'
iteratorA.next() // 'b'
iteratorA.next() // 'a'
iteratorA.next() // 'r'
iteratorB = generatorA();
[...iteratorB] // ["h", "i", "foo", "b", "a", "r"]

Definindo o resultado a expressão yield <value>

Observe o seguinte código:

function myGenerator() {
const yieldedValue = yield 10;
console.log("yieldedValue", yieldedValue);
}
const iterator = myGenerator();
iterator.next(); // { value: 10, done: false }
iterator.next();
// "yieldedValue" undefined
// { value: undefined, done: true }

Por padrão, o resultado da expressão yield <value> retorna undefined, porém podemos sobrescrever este valor passando um argumento na chamada de iterator.next(<value>), como segue abaixo:

const iterator = myGenerator();
iterator.next(); // { value: 10, done: false }
iterator.next("custom value");
// "yieldedValue" "custom value"
// { value: undefined, done: true }

Implementando o protocolo de iteração utilizando generators

Inicialmente neste artigo, implementamos manualmente o protocolo de iteração, como segue abaixo:

const myIterable = {
[Symbol.iterator]: function() {
let limit = 5;
let i = 0;
return {
next: function() {
return {
done: i >= limit,
value: i < limit ? i++ : undefined
}
}
}
}
}

Vamos ver agora como fazer o mesmo utilizando um generator:

const myIterable = {
[Symbol.iterator]: function* () {
let i = 0;
while(i < 5) {
yield i++;
}
}
}
[...myIterable] // [0, 1, 2, 3, 4]

Muito mais simples não?

Outros exemplos

Gerando um range de números

Usar generators também pode ser uma alternativa para criar um Array com um range de valores. Como por exemplo:

function* range(a, b) => {
let n = a;
while(n <= b) {
yield n;
n++;
}
}
[...range(2, 5)] // [2, 3, 4, 5]

Redux Saga

Um biblioteca que admiro muito, e que faz bastante uso desses recursos é o redux-saga, voltado para gerenciar side-effects em aplicações construídas com Redux.

Trabalhar com redux-saga sem ter conhecimentos básicos de generators é complicado. Se você leu esse artigo não se sentirá mais em um mundo estranho ao ler sua documentação.

Se você gostaria entender como o redux-saga faz uso de generators e como funciona essa implementação, leia meu artigo: Como funciona o Redux Saga?

Finalizando

Iterators e Generators são recursos recentes na linguagem e ainda há muito o que ser explorado em possíveis aplicações.

Não deixe de comentar caso tenha dúvidas, sugestões ou correções.

Obrigado e até a próxima!