As funções geradoras, ou generators, em JavaScript, são uma característica poderosa introduzida no ECMAScript 6 (também conhecido como ES6) que oferece uma maneira mais eficiente e elegante de lidar com iterações e fluxos de dados assíncronos. Elas proporcionam uma forma de criar iteradores sob demanda, permitindo a geração de uma sequência de valores de maneira controlada e eficiente.
Um generator é uma função especial que pode ser pausada e retomada durante sua execução. Ele é definido utilizando a palavra-chave function*
, seguida pelo nome da função e pelos parâmetros, se houverem. Dentro do corpo da função geradora, utilizamos a palavra-chave yield
para produzir um valor a cada vez que a função é chamada. O yield
pausa a execução do generator, retornando o valor especificado, e permite que a função seja retomada de onde parou na próxima vez que for chamada.
Aqui está um exemplo simples de um generator em JavaScript:
javascriptfunction* contador() {
let i = 0;
while (true) {
yield i++;
}
}
const iterador = contador();
console.log(iterador.next().value); // Saída: 0
console.log(iterador.next().value); // Saída: 1
console.log(iterador.next().value); // Saída: 2
// E assim por diante...
Neste exemplo, a função contador
é um generator que gera uma sequência infinita de números inteiros começando por 0. Cada vez que chamamos iterador.next()
, o generator produz o próximo número na sequência.
Uma das principais vantagens dos generators é a capacidade de lidar com iterações de maneira mais eficiente e legível, especialmente em casos onde a geração de valores é complexa ou requer lógica assíncrona.
Além disso, generators são frequentemente usados em conjunto com iterables e iterators para criar estruturas de dados iteráveis personalizadas. Um iterable é um objeto que define uma maneira de acessar seus elementos sequencialmente, enquanto um iterator é um objeto que sabe como acessar os elementos de um iterable.
Vamos ver um exemplo de como podemos criar um iterable usando um generator:
javascriptconst meuIterable = {
*[Symbol.iterator]() {
yield 1;
yield 2;
yield 3;
}
};
for (const valor of meuIterable) {
console.log(valor);
}
// Saída:
// 1
// 2
// 3
Neste exemplo, meuIterable
é um objeto que define um método especial Symbol.iterator
, que é um generator responsável por gerar os valores do iterable. Usamos a sintaxe *[Symbol.iterator]() {}
para definir um generator como o iterador do objeto.
Os generators também são úteis para lidar com operações assíncronas de maneira mais limpa e legível, especialmente com o uso de async/await. Por exemplo, podemos usar um generator em conjunto com operações assíncronas para controlar o fluxo de dados de forma síncrona, mesmo quando lidamos com chamadas assíncronas.
Veja um exemplo de como podemos usar um generator com async/await
:
javascriptfunction* gerarDadosAssincronos() {
const resultado1 = yield fazerAlgoAssincrono();
const resultado2 = yield fazerOutraCoisaAssincrona(resultado1);
return resultado2;
}
async function fazerAlgoAssincrono() {
return new Promise(resolve => {
setTimeout(() => resolve(42), 1000);
});
}
async function fazerOutraCoisaAssincrona(valor) {
return new Promise(resolve => {
setTimeout(() => resolve(valor * 2), 1000);
});
}
async function principal() {
const iterador = gerarDadosAssincronos();
const resultado1 = await iterador.next().value;
const resultado2 = await iterador.next(resultado1).value;
const resultadoFinal = await iterador.next(resultado2).value;
console.log(resultadoFinal);
}
principal(); // Saída: 84 (42 * 2)
Neste exemplo, gerarDadosAssincronos
é um generator que controla o fluxo de duas operações assíncronas, fazerAlgoAssincrono
e fazerOutraCoisaAssincrona
. Utilizamos yield
para pausar a execução do generator até que as operações assíncronas sejam concluídas, e await
para esperar o resultado dessas operações.
Em resumo, os generators são uma ferramenta poderosa em JavaScript para lidar com iterações, controle de fluxo e operações assíncronas. Eles fornecem uma maneira elegante e eficiente de produzir valores de forma controlada e podem ser usados para tornar o código mais legível e conciso.
“Mais Informações”
Em JavaScript, os “geradores” (ou “generators”, em inglês) são uma funcionalidade poderosa que permite criar iteradores personalizados. Eles introduzem uma forma de gerar uma sequência de valores de forma preguiçosa, ou seja, um valor de cada vez, sob demanda, ao invés de gerar todos os valores de uma vez só. Essa abordagem é útil quando lidamos com grandes conjuntos de dados ou quando queremos controlar o fluxo de execução de maneira mais granular.
Um gerador em JavaScript é definido através da função function*
, que é uma forma especial de declaração de função. Dentro do corpo do gerador, podemos utilizar a palavra-chave yield
para produzir (ou “yieldar”) valores individualmente. Cada vez que o yield
é encontrado, o estado da função é suspenso, mantendo todo o seu contexto de execução, mas permitindo que o controle seja devolvido ao chamador juntamente com o valor yieldado.
Vamos ver um exemplo simples de como um gerador pode ser definido e utilizado em JavaScript:
javascriptfunction* meuGerador() {
yield 1;
yield 2;
yield 3;
}
const iterador = meuGerador();
console.log(iterador.next()); // Saída: { value: 1, done: false }
console.log(iterador.next()); // Saída: { value: 2, done: false }
console.log(iterador.next()); // Saída: { value: 3, done: false }
console.log(iterador.next()); // Saída: { value: undefined, done: true }
Neste exemplo, a função meuGerador
é um gerador que produz os valores 1, 2 e 3, respectivamente. Cada vez que chamamos iterador.next()
, o gerador é executado até encontrar a próxima instrução yield
, retornando um objeto com a propriedade value
contendo o valor yieldado e a propriedade done
indicando se o gerador já terminou de produzir valores.
Uma característica importante dos geradores é que eles podem ser pausados e retomados, o que os torna úteis para lidar com tarefas assíncronas e eventos. Por exemplo, podemos implementar um gerador para iterar sobre os resultados de uma requisição assíncrona de dados:
javascriptfunction* fetchData() {
const response = yield fetch('https://exemplo.com/dados');
const data = yield response.json();
yield data;
}
const iterador = fetchData();
iterador.next().value.then(response => {
return iterador.next(response).value;
}).then(data => {
return iterador.next(data).value;
}).then(resultado => {
console.log(resultado);
});
Neste exemplo, o gerador fetchData
é utilizado para lidar com uma requisição assíncrona de dados. Cada yield
pausa a execução do gerador até que o valor correspondente seja fornecido externamente. Isso permite um controle mais intuitivo sobre o fluxo de execução assíncrona.
Os geradores também podem ser utilizados em estruturas de controle de fluxo como loops for...of
, o que os torna úteis para processar sequências de maneira eficiente e concisa:
javascriptfunction* gerarNumeros(limite) {
for (let i = 0; i <= limite; i++) {
yield i;
}
}
for (const numero of gerarNumeros(5)) {
console.log(numero);
}
Neste exemplo, o gerador gerarNumeros
é utilizado para produzir números de 0 a um limite especificado. O loop for...of
então itera sobre os valores produzidos pelo gerador, simplificando o código e evitando a necessidade de criar um array com todos os números antecipadamente.
Além disso, os geradores em JavaScript também podem ser combinados com outros recursos da linguagem, como os iteradores, para criar abstrações poderosas e expressivas. Por exemplo, podemos criar um iterador que percorre uma árvore de forma assíncrona utilizando um gerador para controlar o fluxo de execução:
javascriptclass Node {
constructor(value) {
this.value = value;
this.children = [];
}
addChild(value) {
const child = new Node(value);
this.children.push(child);
return child;
}
}
function* traverseTree(node) {
yield node.value;
for (const child of node.children) {
yield* traverseTree(child);
}
}
const root = new Node('A');
const nodeB = root.addChild('B');
const nodeC = root.addChild('C');
nodeB.addChild('D');
nodeB.addChild('E');
nodeC.addChild('F');
const iterador = traverseTree(root);
const proximoValor = () => {
const resultado = iterador.next();
if (!resultado.done) {
console.log(resultado.value);
proximoValor();
}
};
proximoValor();
Neste exemplo, a classe Node
representa um nó de uma árvore, e a função traverseTree
é um gerador que itera sobre todos os nós da árvore de forma assíncrona. A palavra-chave yield*
é utilizada para delegar a iteração dos filhos de cada nó para o mesmo gerador, permitindo uma travessia completa da árvore de forma simples e eficiente.
Em resumo, os geradores em JavaScript são uma ferramenta poderosa para controlar o fluxo de execução, permitindo a criação de iteradores personalizados e simplificando o código assíncrono. Eles oferecem uma abordagem flexível e expressiva para lidar com sequências de valores, tornando o código mais legível e eficiente.