Geradores em JavaScript
Geradores representam um tipo especial de função usados para gerar valores. Para definir geradores, usa-se o símbolo de asterisco * após a palavra-chave function. Vejamos como definir um gerador simples:
function* getNumber() {
  yield 5;
}
const numberGenerator = getNumber();
const result = numberGenerator.next();
console.log(result); // {value: 5, done: false}A função getNumber() é um gerador. Os principais aspectos da criação e uso de um gerador são:
- Um gerador é definido como uma função com o operador - function*(o asterisco vem após a palavra- function).- function* getNumber() { .... }
- Para retornar um valor do gerador, usa-se o operador - yield, seguido pelo valor a ser retornado.- yield 5;- Neste caso, o gerador - getNumber()gera o número 5.
- Para obter um valor do gerador, utiliza-se o método - next().- const result = numberGenerator.next();- Ao chamar a função - getNumber(), cria-se um objeto iterador, denominado- numberGenerator. Com esse objeto, podemos obter valores do gerador.
Ao observar a saída do console, vemos que este método retorna:
{value: 5, done: false}Isso indica que o objeto retornado tem uma propriedade value que contém o valor gerado e uma propriedade done que indica se o fim do gerador foi alcançado.
Os geradores são semelhantes aos iteradores, mas são uma forma especial de iteradores.
Vamos alterar o código:
function* getNumber() {
  yield 5;
}
const numberGenerator = getNumber();
let next = numberGenerator.next();
console.log(next);
next = numberGenerator.next();
console.log(next);A chamada ao método next() ocorre duas vezes:
{value: 5, done: false}
{value: undefined, done: true}Mas a função geradora getNumber() gera apenas um valor, o número 5. Assim, em uma chamada subsequente, a propriedade value terá o valor undefined e a propriedade done será true, indicando que o gerador concluiu sua execução.
Um gerador pode criar/gerar vários valores:
function* getNumber() {
  yield 5;
  yield 25;
  yield 125;
}
const numberGenerator = getNumber();
console.log(numberGenerator.next());
console.log(numberGenerator.next());
console.log(numberGenerator.next());
console.log(numberGenerator.next());Saída do console:
{value: 5, done: false}
{value: 25, done: false}
{value: 125, done: false}
{value: undefined, done: true}Para simplificar, podemos retornar elementos de um array em um gerador:
const numbers = [5, 25, 125, 625];
function* getNumber() {
  for (const n of numbers) {
    yield n;
  }
}
const numberGenerator = getNumber();
console.log(numberGenerator.next().value); // 5
console.log(numberGenerator.next().value); // 25Entre duas chamadas consecutivas de next(), pode haver uma pausa indeterminada, durante a qual outras ações podem ocorrer, mas o gerador continuará a retornar o próximo valor:
const numberGenerator = getNumber();
console.log(numberGenerator.next().value); // 5
// outras ações
console.log(numberGenerator.next().value); // 25Geradores não se limitam apenas aos operadores yield. Eles também podem conter lógicas mais complexas.
Geradores são úteis para criar sequências infinitas:
function* points() {
  let x = 0;
  let y = 0;
  while (true) {
    yield { x: x, y: y };
    x += 2;
    y += 1;
  }
}
let pointGenerator = points();
console.log(pointGenerator.next().value);
console.log(pointGenerator.next().value);
console.log(pointGenerator.next().value);Saída do console:
{x: 0, y: 0}
{x: 2, y: 1}
{x: 4, y: 2}Retorno do Gerador e a Função return
Como vimos anteriormente, cada chamada subsequente ao método next()retorna o próximo valor do gerador. No entanto, podemos encerrar a execução do gerador utilizando o método return():
function* getNumber() {
  yield 5;
  yield 25;
  yield 125;
}
const numberGenerator = getNumber();
console.log(numberGenerator.next()); // {value: 5, done: false}
numberGenerator.return(); // encerramos a execução do gerador
console.log(numberGenerator.next()); // {value: undefined, done: true}Obtendo Valores do Gerador em um Loop
Como iteradores são utilizados para obter valores, podemos usar o loop for..of:
function* getNumber() {
  yield 5;
  yield 25;
  yield 125;
}
const numberGenerator = getNumber();
for (const num of numberGenerator) {
  console.log(num);
}Saída do console:
5 25 125
Também podemos usar outros tipos de loops, como o loop while:
function* getNumber() {
  yield 5;
  yield 25;
  yield 125;
}
const numberGenerator = getNumber();
let item;
while (!(item = numberGenerator.next()).done) {
  console.log(item.value);
}Passando Dados para o Gerador
Inicialização do Gerador
Assim como qualquer outra função, a função geradora pode aceitar parâmetros. Através dos parâmetros, podemos passar dados para o gerador. Por exemplo:
function* getNumber(start, end, step) {
  for (let n = start; n <= end; n += step) {
    yield n;
  }
}
const numberGenerator = getNumber(0, 8, 2);
for (const num of numberGenerator) {
  console.log(num);
}Saída do console:
0 2 4 6 8
Outro exemplo é definir um gerador que retorna dados de um array:
function* generateFromArray(items) {
  for (const item of items) yield item;
}
const people = ["Tom", "Bob", "Sam"];
const personGenerator = generateFromArray(people);
for (const person of personGenerator) console.log(person);Saída do console:
Tom Bob Sam
Passando Dados para o Método next
Com o método next(), podemos passar dados para o gerador. Os dados enviados a esse método podem ser capturados na função geradora através da chamada anterior do operador yield:
function* getNumber() {
  const n = yield 5; // recebe o valor de numberGenerator.next(2).value
  console.log("n:", n);
  const m = yield 5 * n; // recebe o valor de numberGenerator.next(3).value
  console.log("m:", m);
  yield 5 * m;
}
const numberGenerator = getNumber();
console.log(numberGenerator.next().value); // 5
console.log(numberGenerator.next(2).value); // 10
console.log(numberGenerator.next(3).value); // 15Saída do console:
5 n: 2 10 m: 3 15
No segundo chamado do método next():
numberGenerator.next(2).value;Os dados passados por ele podem ser capturados atribuindo o resultado da primeira chamada do operador yield:
Assim, a constante n será igual a 2, já que o número 2 é passado para o método next().
Depois, podemos usar esse valor para gerar um novo valor:
const m = yield 5 * n;A constante m então receberá o valor passado pelo terceiro chamado do método next(), que é o número 3.
Tratamento de Erros em Geradores
Com a função throw(), podemos lançar uma exceção dentro do gerador. Um valor arbitrário, que representa a informação sobre o erro, é passado como parâmetro para essa função:
function* generateData() {
  try {
    yield "Tom";
    yield "Bob";
    yield "Hello Work";
  } catch (error) {
    console.log("Error:", error);
  }
}
const personGenerator = generateData();
console.log(personGenerator.next()); // {value: "Tom", done: false{
personGenerator.throw("Something wrong"); // Error: Something wrong
console.log(personGenerator.next()); // {value: undefined, done: true{Primeiramente, na função geradora, usamos a construção try..catch para lidar com possíveis exceções. No bloco catch, com o parâmetro error, podemos obter a informação sobre o erro que é passado para a função throw().
Quando usamos o gerador, podemos chamar essa função, passando informações arbitrárias sobre o erro (neste caso, é apenas uma mensagem de texto):
personGenerator.throw("Something wrong");Essa chamada resultará em uma exceção na função geradora, e o controle será transferido para o bloco catch, que imprime a informação sobre o erro no console:
{value: "Tom", done: false}
Error: Something wrong
{value: undefined, done: true}Vale ressaltar que após a chamada da função throw(), o gerador encerra sua execução, e na subsequente chamada do método next(), obteremos o resultado {value: undefined, done: true}.