Menu

Iterators e Generators em JavaScript: function* e yield

Entenda como funciona o protocolo de iteração do JavaScript, como tornar seus próprios objetos iteráveis e como as generator functions deixam tudo isso muito mais simples.

O protocolo de iteração

Várias features do JavaScript — for...of, spread (...), desestruturação, Array.from, Promise.all — compartilham um mecanismo em comum por baixo dos panos: o protocolo de iteração. Depois que você entende como ele funciona, todas essas features passam a parecer variações da mesma ideia.

Um iterator é qualquer objeto que tenha um método next() retornando { value, done }:

index.js
Output
Click Run to see the output here.

Chame next() várias vezes. Cada chamada devolve o próximo valor e uma flag done. Quando done for true, a sequência acabou. Esse é todo o protocolo de iteração — quatro toques no teclado e um booleano.

Iterable vs iterator no JavaScript

Existe um segundo conceito, bem próximo desse. Um iterable é qualquer coisa que saiba produzir um iterator. Ele faz isso através de um método guardado sob uma chave especial: Symbol.iterator.

index.js
Output
Click Run to see the output here.

Arrays são iteráveis. Chamar numbers[Symbol.iterator]() devolve um iterator novinho em folha. Strings, Map, Set e arguments também são iteráveis — é justamente por isso que o for...of funciona em todos eles.

Essa separação faz diferença: o iterable é a coleção, o iterator é o cursor que caminha por ela. Você pode pedir quantos cursores independentes quiser a partir de um mesmo iterável.

Por que o for...of funciona

O for...of nada mais é do que um açúcar sintático em cima do protocolo de iteração do JavaScript. Por baixo dos panos, ele chama Symbol.iterator e vai invocando next() até que done seja true:

index.js
Output
Click Run to see the output here.

Tanto o spread quanto o destructuring fazem a mesma coisa por baixo dos panos: percorrem o iterator até ele terminar:

index.js
Output
Click Run to see the output here.

Qualquer objeto que você construir implementando Symbol.iterator já entra de brinde em todos esses recursos.

Criando um iterable personalizado em JavaScript

Bora montar um objeto range que devolve números de start até end:

index.js
Output
Click Run to see the output here.

Algumas coisas que vale a pena observar:

  • [Symbol.iterator]() usa um nome de método computado. A chave é o próprio símbolo, não a string "Symbol.iterator".
  • Cada chamada a [Symbol.iterator]() devolve um iterator novinho em folha, com seu próprio current. É justamente isso que te permite percorrer range duas vezes sem que ele fique "esgotado".
  • O iterator retornado só precisa de next(). Só isso.

Funciona, mas é verboso. Existe um jeito bem melhor.

Chegaram os generators

Uma generator function é declarada com function* (repare na estrela). Em vez de rodar do começo ao fim, ela consegue pausar numa expressão yield e retomar depois. Chamá-la não executa o corpo — ela devolve um generator object, que é ao mesmo tempo um iterator e um iterable:

index.js
Output
Click Run to see the output here.

Cada next() executa o corpo da função até topar em um yield, pausa a execução e devolve { value, done: false }. Quando a função chega ao fim, você recebe { value: undefined, done: true }.

E como generators já são iteráveis por natureza, eles funcionam com tudo que vimos na seção anterior:

index.js
Output
Click Run to see the output here.

Reescrevendo o range com um generator

Compare a versão verbosa acima com esta:

index.js
Output
Click Run to see the output here.

É isso. O * antes de [Symbol.iterator] transforma o método em um generator. O yield i substitui todo aquele objeto iterator feito na mão. Sem next, sem done, sem risco de errar o índice por um — é só um loop comum usando yield no lugar de push.

É exatamente pra isso que os generators existem. Eles transformam "escrever um iterator" em "escrever uma função que dá yield".

yield vs return no JavaScript

O yield pausa; o return encerra. Você pode usar yield quantas vezes quiser — o generator continua de onde parou:

index.js
Output
Click Run to see the output here.

Um return dentro de um generator aparece como { value: "done", done: true } na chamada que o encerra. O for...of e o spread ignoram esse valor retornado — eles só consomem itens enquanto done for false. Então não tente usar return valor para contrabandear um item final dentro do loop; ele vai ser ignorado.

Sequências preguiçosas e infinitas

Generators produzem valores sob demanda, um de cada vez. Isso permite representar sequências que seriam impossíveis como arrays:

index.js
Output
Click Run to see the output here.

O loop é literalmente while (true), mas ainda assim o programa termina — isso acontece porque o generator só avança quando alguém pede o próximo valor. Dá pra pegar os N primeiros itens, parar, e o resto simplesmente nunca roda:

index.js
Output
Click Run to see the output here.

take é ela mesma uma generator que envolve outra. Compor generators assim é uma das grandes sacadas — peças pequenas, cada uma com uma única responsabilidade.

Delegando com yield*

Quando uma generator precisa produzir todos os valores de outro iterable, o yield* delega essa tarefa:

index.js
Output
Click Run to see the output here.

yield* funciona com qualquer iterável — arrays, sets, outros generators — e repassa cada item, um de cada vez. É o equivalente do spread para iterators.

Async generators, rapidinho

Um generator declarado como async function* consegue dar yield em valores que demoram pra chegar — bem útil pra fazer streaming de uma API ou ler pedaços de um arquivo. Você consome ele com for await...of:

async function* paginate(url) {
  let next = url;
  while (next) {
    const res = await fetch(next);
    const page = await res.json();
    for (const item of page.items) yield item;
    next = page.nextUrl;
  }
}

for await (const item of paginate("/api/users")) {
  console.log(item);
}

Esse trecho não roda aqui (precisa de um endpoint de verdade), mas vale conhecer o formato. Depois que você pegar o jeito dos generators comuns, os async generators são a mesma ideia, só que com await espalhado no meio.

Quando usar um generator

Vale a pena quando:

  • A sequência é infinita ou pode ser — IDs, timestamps, tempos de retry.
  • Gerar todos os valores é caro e o consumidor pode parar no meio do caminho.
  • Você está implementando Symbol.iterator em um objeto personalizado. Quase sempre fica mais curto do que montar o objeto { next() } na mão.
  • Você quer compor transformações em streaming (take, filter, map) sem criar arrays intermediários.

Prefira um array comum quando os dados já estão na memória e o volume é pequeno. Generators não saem de graça — toda essa engenharia de suspender e retomar uma função tem um custo, e stack traces passando por código de generator podem ser mais chatos de ler.

A seguir: Symbols

Symbol.iterator costuma ser o primeiro símbolo com que a gente esbarra, mas está longe de ser o único. Symbols são um tipo primitivo pensado justamente para esse tipo de ponto de extensão — chaves únicas que permitem à linguagem (e ao seu próprio código) se conectar a objetos sem colidir com nomes de propriedade comuns. É o assunto da próxima página.

Perguntas frequentes

Qual a diferença entre iterable e iterator no JavaScript?

Um iterable é qualquer objeto que tenha o método Symbol.iterator, e esse método devolve um iterator. Já o iterator é o objeto que realmente produz os valores — ele tem um método next() que retorna { value, done }. Arrays, strings, Map e Set são iterables; quando você chama o Symbol.iterator deles, recebe um iterator para percorrer.

O que é uma generator function no JavaScript?

É uma função declarada com function* que produz valores de forma preguiçosa (lazy) usando yield. Chamar a função não executa o corpo dela — o que você recebe é um objeto generator, que é ao mesmo tempo um iterator e um iterable. A cada chamada de next(), o código roda até o próximo yield, pausa ali e devolve o valor.

Qual a diferença entre yield e return dentro de um generator?

O yield pausa o generator e devolve um valor, mas a função pode retomar exatamente de onde parou na próxima chamada de next(). Já o return encerra o generator de vez — ele marca done: true e nenhum valor novo sai depois disso. Você pode usar yield várias vezes; return, na prática, só uma.

Quando vale a pena usar um generator em vez de um array?

Quando a sequência é infinita, cara de calcular ou você só precisa de alguns valores. O generator produz um item de cada vez, sob demanda, então dá pra representar uma sequência infinita de IDs ou resultados paginados de uma API sem carregar tudo de uma vez na memória. Se você já tem um array pequeno e fixo, fica no array mesmo.

Aprenda a programar com o Coddy

COMEÇAR