Iteratores e geradores

Processar cada item em uma coleção é uma operação muito comum. O JavaScript disponibiliza uma série de maneiras de iterar sobre uma coleção, desde um simples laço for, até um map() e também com o filter(). Iteradores e Geradores trazem o conceito da interação ocorrer diretamente no núcleo da linguagem e prover um mecanismo para a customização do comportamento dos laços for...of.

Para detalhes, também veja:

Iterators (Iteradores)

Um objeto é um iterator (iterador) quando sabe como acessar itens numa coleção, um por vez, enquanto mantém rastreada a posição atual em uma dada sequência. Em JavaScript um iterator é um objeto que oferece o método next(), o qual retorna o próximo item da sequência. Este método retorna um objeto com duas propriedades: done e value.

Uma vez criado, um objeto iterator pode ser usado explicitamente ao chamar repetidas vezes o método next().

js
const makeIterator = (array) => {
  let nextIndex = 0;

  return {
    next: () => {
      return nextIndex < array.length
        ? { value: array[nextIndex++], done: false }
        : { done: true };
    },
  };
};

Uma vez inicializado, o método next() pode ser chamado para acessar os pares chave/valor do objeto da vez.

js
let it = makeIterator(["yo", "ya"]);
console.log(it.next().value); // 'yo'
console.log(it.next().value); // 'ya'
console.log(it.next().done); // true

Iterables (Iteráveis)

Um objeto é iterável (iterable), se ele define seu comportamento de iteração, como no caso de quais valores percorridos em um laço do tipo for..of. Alguns tipos embutidos, como o Array, ou o Map, têm um comportamento iterável padrão, enquanto outros tipos (como oObject) não possuem.

Para que um objeto seja iterable, o objeto precisa implementar o método @@iterator, significando que o objeto (ou um dos objetos na cadeia de prototipagem - prototype chain) precisa ter uma propriedade com uma chave Symbol.iterator:

Iterables definidos pelo usuário

Você pode fazer seus próprios iteráveis da seguinte maneira:

js
var myIterable = {};
myIterable[Symbol.iterator] = function* () {
  yield 1;
  yield 2;
  yield 3;
};
[...myIterable]; // [1, 2, 3]

Iterables Built-in (Iteráveis Embutidos)

String, Array, TypedArray, Map e Set são iteráveis embutidos, pois o protótipo dos objetos de todos eles têm o método Symbol.iterator.

Syntaxes expecting iterables

Algumas declarações e expressões esperam por iteradores, por exemplo o for-of loops, spread operator, yield*, e destructuring assignment.

js
for (let value of ["a", "b", "c"]) {
  console.log(value);
}
// "a"
// "b"
// "c"

[..."abc"]; // ["a", "b", "c"]

function* gen() {
  yield* ["a", "b", "c"];
}

gen().next(); // { value:"a", done:false }

[(a, b, c)] = new Set(["a", "b", "c"]);
a; // "a"

Generators

Enquanto os iteradores são ferramentas muito úteis, sua criação requer um cuidado devido à necessidade de manter explícito seu estado interno. Generators provêm uma alternativa poderosa: eles te permitem definir um algoritmo iterativo escrevendo uma função simples que pode manter seu estado próprio.

Generator é um tipo especial de função que trabalha como uma factory para iteradores. A função vira um generator se ela contém uma ou mais expressões yield e se ela usa a sintaxe function*.

js
function* idMaker() {
  var index = 0;
  while (true) yield index++;
}

var gen = idMaker();

console.log(gen.next().value); // 0
console.log(gen.next().value); // 1
console.log(gen.next().value); // 2
// ...

Generators avançados

Generators computam seus valores "yielded" por demanda, que os permitem representar sequências de forma eficiente que costumam ser trabalhosas ao serem computadas, ou até sequências infinitas como demonstradas acima.

O método next() também aceita um valor que pode ser usado para modificar o estado interno de um generator. O valor passado pro next() será tratado como o resultado da última expressão yield que pausou o generator.

Aqui um gerador de sequência Fibonacci usando next(x) pra restartar a sequência:

js
function* fibonacci() {
  var fn1 = 1;
  var fn2 = 1;
  while (true) {
    var current = fn2;
    fn2 = fn1;
    fn1 = fn1 + current;
    var reset = yield current;
    if (reset) {
      fn1 = 1;
      fn2 = 1;
    }
  }
}

var sequence = fibonacci();
console.log(sequence.next().value); // 1
console.log(sequence.next().value); // 1
console.log(sequence.next().value); // 2
console.log(sequence.next().value); // 3
console.log(sequence.next().value); // 5
console.log(sequence.next().value); // 8
console.log(sequence.next().value); // 13
console.log(sequence.next(true).value); // 1
console.log(sequence.next().value); // 1
console.log(sequence.next().value); // 2
console.log(sequence.next().value); // 3

Nota: Como um ponto de interesse, chamando next(undefined) é o mesmo que chamar next(). Entretanto, estartar um novo generator com qualquer valor que não seja undefined na chamada next() terá TypeError exception.

Você pode forçar um generator a lançar uma exceção chamando o seu método throw() e passando o valor da exceção que ele deve lançar. Essa exceção será lançada do contexto suspenso atual do generator, como se o yield atualmente suspenso fosse um throw.

Se um yield não for encontrado durante o processo de lançamento de um thrown exception, então o exception será propagado através da chamada do throw(), e pra subsequente chamada do next() que terá a propriedade done resultando em true.

Generators têm o método return(value) que retorna o valor pego e finaliza o generator.