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 palavrafunction
).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, denominadonumberGenerator
. 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); // 25
Entre 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); // 25
Geradores 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); // 15
Saí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}
.