Com a introdução de ES6 , iteradores e geradores foram oficialmente adicionados ao JavaScript.

Os iteradores permitem que você itere sobre qualquer objeto que siga a especificação. Na primeira seção, veremos como usar iteradores e tornar qualquer objeto iterável.

A segunda parte desta postagem do blog se concentra inteiramente nos geradores: o que são, como usá-los e em que situações podem ser úteis.

Sempre gosto de ver como as coisas funcionam nos bastidores: em uma série de blogs anteriores, expliquei como o JavaScript funciona no navegador . Como continuação disso, quero explicar como os iteradores e geradores de JavaScript funcionam neste artigo.

O que são iteradores?

Antes de podermos entender os geradores, precisamos ter um entendimento completo dos iteradores em JavaScript, pois esses dois conceitos andam de mãos dadas. Após esta seção, ficará claro que os geradores são simplesmente uma maneira de escrever iteradores com mais segurança.

Como o nome já indica, os iteradores permitem iterar sobre um objeto (arrays também são objetos).

Provavelmente, você já usou iteradores JavaScript. Sempre que você iterou em uma matriz, por exemplo, usou iteradores, mas também pode iterar em objetos Map e até em strings.

 para (deixe i de'abc') { console.log (i);
} //Resultado
//"uma"
//"b"
//"c"

Qualquer objeto que implemente o protocolo iterável pode ser iterado usando “para… de”.

Indo um pouco mais fundo, você pode tornar qualquer objeto iterável implementando a função @@ iterator , que retorna um objeto iterador .

Tornando qualquer objeto iterável

Para entender isso corretamente, provavelmente é melhor dar uma olhada em um exemplo de como tornar um objeto regular iterável.

Começamos com um objeto que contém nomes de usuários agrupados por cidade:

 const userNamesGroupedByLocation={ Tokio: [ 'Aiko', 'Chizu', 'Fushigi', ], 'Buenos Aires': [ 'Santiago', 'Valentina', 'Lola', ], 'São Petersburgo': [ 'Sonja', 'Dunja', 'Iwan', 'Tanja', ],
};

Peguei este exemplo porque não é fácil iterar nos usuários se os dados forem estruturados dessa forma; para fazer isso, precisaríamos de vários loops para obter todos os usuários.

Se tentarmos iterar sobre este objeto como ele está, obteremos a seguinte mensagem de erro:

 ▶ ReferenceError não capturado: iterador não definido

Para tornar este objeto iterável, primeiro precisamos adicionar a função @@ iterator . Podemos acessar este símbolo via Symbol.iterator .

 userNamesGroupedByLocation [Symbol.iterator]=function () { //...
}

Como mencionei antes, a função iteradora retorna um objeto iterador. O objeto contém uma função em next , que também retorna um objeto com dois atributos: done e value .

 userNamesGroupedByLocation [Symbol.iterator]=function () { Retorna { próximo: ()=& gt; { Retorna { feito: verdadeiro, valor:'oi', }; }, };
}

value contém o valor atual da iteração, enquanto done é um booleano que nos informa se a execução foi concluída.

Ao implementar esta função, precisamos ser especialmente cuidadosos com o valor done , pois ele sempre retorna false e resultará em um loop infinito.

O exemplo de código acima já representa uma implementação correta do protocolo iterável. Podemos testá-lo chamando a função next do objeto iterador.

//Chamar a função iteradora retorna o objeto iterador
const iterator=userNamesGroupedByLocation [Symbol.iterator] ();
console.log (iterator.next (). valor);
//"Oi"

Iterando sobre um objeto com “for… of” usa a função next sob o capô.

Usar “for… of” neste caso não retornará nada porque definimos imediatamente done como false . Também não obtemos nenhum nome de usuário ao implementá-lo dessa forma, e é por isso que queríamos tornar esse objeto iterável em primeiro lugar.

Implementando a função iteradora

Em primeiro lugar, precisamos acessar as chaves do objeto que representa as cidades. Podemos obter isso chamando Object.keys na palavra-chave this , que se refere ao pai da função, que, neste caso, é o userNamesGroupedByLocation objeto.

Só podemos acessar as chaves por meio de this se definirmos a função iterável com a palavra-chave function . Se usássemos uma função de seta, isso não funcionaria porque eles herdam o escopo de seus pais.

 const cityKeys=Object.keys (this);

Também precisamos de duas variáveis ​​que controlam nossas iterações.

 deixe cityIndex=0;
deixe userIndex=0;

Definimos essas variáveis ​​na função iteradora, mas fora da função next , que nos permite manter os dados entre as iterações.

Na função next , primeiro precisamos obter a matriz de usuários da cidade atual e o usuário atual, usando os índices que definimos antes.

Podemos usar esses dados para alterar o valor de retorno agora.

 return { próximo: ()=& gt; { const users=this [cityKeys [cityIndex]]; const user=users [userIndex]; Retorna { feito: falso, valor: usuário, }; },
};

A seguir, precisamos incrementar os índices a cada iteração.

Incrementamos o índice do usuário todas as vezes, a menos que tenhamos chegado ao último usuário de uma determinada cidade, nesse caso definiremos userIndex como 0 e incrementaremos a cidade em vez disso, indexe.

 return { próximo: ()=& gt; { const users=this [cityKeys [cityIndex]]; const user=users [userIndex]; const isLastUser=userIndex & gt;=users.length-1; if (isLastUser) { //Redefinir o índice do usuário userIndex=0; //Pula para a próxima cidade cityIndex ++ } senão { userIndex ++; } Retorna { feito: falso, valor: usuário, }; },
};

Tenha cuidado para não iterar neste objeto com “para… de”. Dado que done é sempre igual a false , isso resultará em um loop infinito.

A última coisa que precisamos adicionar é uma condição de saída que defina done como true . Saímos do loop depois de ter iterado em todas as cidades.

 if (cityIndex & gt; cityKeys.length-1) { Retorna { valor: indefinido, feito: verdadeiro, };
}

Depois de colocar tudo junto, nossa função se parece com o seguinte:

 userNamesGroupedByLocation [Symbol.iterator]=function () { const cityKeys=Object.keys (this); deixe cityIndex=0; deixe userIndex=0; Retorna { próximo: ()=& gt; { //Já iteramos em todas as cidades if (cityIndex & gt; cityKeys.length-1) { Retorna { valor: indefinido, feito: verdadeiro, }; } const users=this [cityKeys [cityIndex]]; const user=users [userIndex]; const isLastUser=userIndex & gt;=users.length-1; userIndex ++; if (isLastUser) { //Redefinir o índice do usuário userIndex=0; //Pula para a próxima cidade cityIndex ++ } Retorna { feito: falso, valor: usuário, }; }, };
};

Isso nos permite obter rapidamente todos os nomes de nosso objeto usando um loop “for… of”.

 para (deixe o nome de userNamesGroupedByLocation) { console.log ('nome', nome);
} //Resultado:
//nomeie Aiko
//nomeie Chizu
//nomeie Fushigi
//nomeie Santiago
//nomeie Valentina
//nomeie Lola
//nomeie Sonja
//nomeie Dunja
//nomeie Iwan
//nomeie Tanja

Como você pode ver, tornar um objeto iterável não é mágico. No entanto, isso precisa ser feito com muito cuidado porque erros na função next podem facilmente levar a um loop infinito.

Se você deseja aprender mais sobre o comportamento, encorajo você a tentar tornar um objeto de sua escolha iterável também. Você pode encontrar uma versão executável do código neste tutorial neste codepen .

Para resumir o que fizemos para criar um iterável, aqui estão as etapas que seguimos:

  • Adicione uma função iteradora ao objeto com a chave @@ iterator (acessível por meio de Symbol.iterator
  • Essa função retorna um objeto que inclui uma função próxima
  • A função next retorna um objeto com os atributos done e value

O que são geradores?

Aprendemos como tornar qualquer objeto iterável, mas como isso se relaciona com os geradores?

Embora os iteradores sejam uma ferramenta poderosa, não é comum criá-los como fizemos no exemplo acima. Precisamos ter muito cuidado ao programar iteradores, pois bugs podem ter consequências sérias e gerenciar a lógica interna pode ser um desafio.

Os geradores são uma ferramenta útil que nos permite criar iteradores definindo uma função.

Essa abordagem é menos sujeita a erros e nos permite criar iteradores com mais eficiência.

Uma característica essencial dos geradores e iteradores é que eles permitem que você pare e continue a execução conforme necessário. Veremos alguns exemplos nesta seção que fazem uso desse recurso.

Declarando uma função geradora

A criação de uma função geradora é muito semelhante às funções regulares. Tudo o que precisamos fazer é adicionar um asterisco ( * ) antes do nome.

função

 * generator () { //...
}

Se quisermos criar uma função geradora anônima, este asterisco vai para o final da palavra-chave função .

função

 * () { //...
}

Usando a palavra-chave yield

Declarar uma função geradora é apenas metade do trabalho e não é muito útil por si só.

Conforme mencionado, os geradores são uma maneira mais fácil de criar iteráveis. Mas como o iterador sabe sobre qual parte da função ele deve iterar? Deve iterar em cada linha?

É aí que a palavra-chave yield entra em ação. Você pode pensar nela como a palavra-chave await que você conhece do JavaScript Promises, mas para geradores.

Podemos adicionar essa palavra-chave a cada linha em que desejamos que a iteração pare. A função next , então, retornará o resultado da instrução dessa linha como parte do objeto iterador ( {done: false, value:'something'} ).

 function * stringGenerator () { rendimento'oi'; rendimento'oi'; rendimento'oi';
} const strings=stringGenerator (); console.log (strings.next ());
console.log (strings.next ());
console.log (strings.next ());
console.log (strings.next ());

A saída deste código será a seguinte:

 {valor:"oi", feito: falso}
{valor:"oi", feito: falso}
{valor:"oi", feito: falso}
{valor: indefinido, concluído: verdadeiro}

Chamar stringGenerator não fará nada por conta própria porque irá parar automaticamente a execução na primeira instrução yield .

Quando a função chega ao fim, value é igual a undefined e done é automaticamente definido como true .

Usando o rendimento *

Se adicionarmos um asterisco à palavra-chave de rendimento, delegamos a execução a outro objeto iterador.

Por exemplo, podemos usar isso para delegar a outra função ou matriz:

 function * nameGenerator () { produzir'Iwan'; rendimento'Aiko';
} function * stringGenerator () { rendimento * nameGenerator (); rendimento * ['um','dois']; rendimento'oi'; rendimento'oi'; rendimento'oi';
} const strings=stringGenerator (); para (deixe o valor das strings) { console.log (valor);
}

O código produz a seguinte saída:

 Iwan
Aiko
1
dois
Oi
Oi
Oi

Passando valores para geradores

A função next que o iterador retorna para geradores tem um recurso adicional: permite que você sobrescreva o valor retornado.

Pegando o exemplo anterior, podemos substituir o valor que yield teria retornado de outra forma.

função

 * overrideValue () { resultado const=rendimento'hi'; console.log (resultado);
} const overrideIterator=overrideValue ();
overrideIterator.next ();
overrideIterator.next ('tchau');

Precisamos chamar next uma vez antes de passar um valor para iniciar o gerador.

Métodos geradores

Além do método “próximo”, que qualquer iterador requer, os geradores também fornecem um return e função throw .

A função de retorno

Chamar return em vez de next em um iterador fará com que o loop saia na próxima iteração.

Cada iteração que vem após a chamada de return definirá done como true e value como undefined .

Se passarmos um valor para esta função, ela substituirá o atributo valor no objeto iterador.

Este exemplo dos documentos Web MDN ilustra isso perfeitamente:

função

 * gen () { rendimento 1; rendimento 2; rendimento 3;
} const g=gen (); g.next ();//{valor: 1, feito: falso}
g.return ('foo');//{valor:"foo", feito: verdadeiro}
g.next ();//{valor: indefinido, feito: verdadeiro}

A função de lançamento

Os geradores também implementam uma função throw , que, em vez de continuar com o loop, lançará um erro e encerrará a execução:

 function * errorGenerator () { tentar { produza'um'; produzir'dois'; } catch (e) { console.error (e); }
} const errorIterator=errorGenerator (); console.log (errorIterator.next ());
console.log (errorIterator.throw ('Bam!'));

O resultado do código acima é o seguinte:

 {valor:'um', feito: falso}
Bam!
{valor: indefinido, concluído: verdadeiro}

Se tentarmos iterar mais depois de lançar um erro, o valor retornado será indefinido e done será definido como true .

Por que usar geradores?

Como vimos neste artigo, podemos usar geradores para criar iteráveis. O tópico pode parecer muito abstrato e devo admitir que raramente preciso usar geradores.

No entanto, alguns casos de uso se beneficiam imensamente desse recurso. Esses casos normalmente aproveitam o fato de que você pode pausar e retomar a execução de geradores.

Gerador de ID exclusivo

Este é meu caso de uso favorito porque se encaixa perfeitamente em geradores.

A geração de IDs exclusivos e incrementais requer que você acompanhe os IDs que foram gerados.

Com um gerador, você pode criar um loop infinito que cria um novo ID a cada iteração.

Sempre que precisar de um novo ID, você pode chamar a função next , e o gerador cuida do resto:

 function * idGenerator () { deixe i=0; while (true) { rendimento i ++; }
} const ids=idGenerator (); console.log (ids.next (). valor);//0
console.log (ids.next (). valor);//1
console.log (ids.next (). valor);//2
console.log (ids.next (). valor);//3
console.log (ids.next (). valor);//4

Obrigado, Nick, pela a ideia .

Outros casos de uso para geradores

Existem muitos outros casos de uso também. Como descobri neste artigo , as máquinas de estado finito também podem usar geradores.

Muitas bibliotecas também usam geradores, como Mobx-State-Tree ou Redux-Saga , por exemplo.

Você encontrou algum outro caso de uso interessante? Deixe-me saber na seção de comentários abaixo.

Conclusão

Geradores e iteradores podem não ser algo que precisamos usar todos os dias, mas quando encontramos situações que exigem seus recursos exclusivos, saber como usá-los pode ser uma grande vantagem.

Neste artigo, aprendemos sobre iteradores e como tornar qualquer objeto iterável. Na segunda seção, aprendemos o que são geradores, como usá-los e em que situações podemos usá-los.

Se quiser saber mais sobre como o JavaScript funciona nos bastidores, você pode conferir minha série de blog em como JavaScript funciona no navegador , explicando o loop de eventos e o gerenciamento de memória do JavaScript.

Leitura adicional:

A postagem Iteradores e geradores de JavaScript: um guia completo apareceu primeiro no LogRocket Blog .

Source link