Como usar geradores e iteradores em JavaScript
Iterar sobre coleções de dados usando loops tradicionais pode rapidamente se tornar complicado e lento, especialmente ao lidar com grandes quantidades de dados.
Geradores e iteradores de JavaScript fornecem uma solução para iterar eficientemente em grandes coleções de dados. Usando-os, você pode controlar o fluxo de iteração, produzir valores um por vez, pausar e retomar o processo de iteração.
Aqui você abordará o básico e interno de um iterador JavaScript e como gerar um iterador manualmente e usando um gerador.
Iteradores de JavaScript
Um iterador é um objeto JavaScript que implementa o protocolo do iterador. Esses objetos fazem isso por ter um próximo método. Este método retorna um objeto que implementa a interface IteratorResult .
A interface IteratorResult compreende duas propriedades: done e value . A propriedade done é um booleano que retorna false se o iterador puder produzir o próximo valor em sua sequência ou true se o iterador tiver concluído sua sequência.
A propriedade value é um valor JavaScript retornado pelo iterador durante sua sequência. Quando um iterador completa sua sequência (quando concluído === true ), essa propriedade retorna undefined .
Como o nome indica, os iteradores permitem que você “itere” sobre objetos JavaScript, como arrays ou mapas. Esse comportamento é possível devido ao protocolo iterável.
Em JavaScript, o protocolo iterável é uma forma padrão de definir objetos sobre os quais você pode iterar, como em um loop for…of .
Por exemplo:
const fruits = ["Banana", "Mango", "Apple", "Grapes"];
for (const iterator of fruits) {
console.log(iterator);
}
/*
Banana
Mango
Apple
Grapes
*/
Este exemplo itera sobre a matriz de frutas usando um loop for…of . Em cada iteração, ele registra o valor atual no console. Isso é possível porque os arrays são iteráveis.
Alguns tipos de JavaScript, como Arrays, Strings, Sets e Maps, são iteráveis integrados porque eles (ou um dos objetos em sua cadeia de protótipos) implementam um método @@ iterator .
Outros tipos, como Objects, não são iteráveis por padrão.
Por exemplo:
const iterObject = {
cars: ["Tesla", "BMW", "Toyota"],
animals: ["Cat", "Dog", "Hamster"],
food: ["Burgers", "Pizza", "Pasta"],
};
for (const iterator of iterObject) {
console.log(iterator);
}
// TypeError: iterObject is not iterable
Este exemplo demonstra o que acontece quando você tenta iterar sobre um objeto que não é iterável.
Tornando um objeto iterável
Para tornar um objeto iterável, você deve implementar um método Symbol.iterator no objeto. Para se tornar iterável, esse método deve retornar um objeto que implemente a interface IteratorResult .
Os blocos de código abaixo fornecem um exemplo de como tornar um objeto iterável usando o iterObject .
Primeiro, adicione o método Symbol.iterator a iterObject usando uma declaração de função.
Igual a:
iterObject[Symbol.iterator] = function () {
// Subsequent code blocks go here...
}
Em seguida, você precisará acessar todas as chaves no objeto que deseja tornar iterável. Você pode acessar as chaves usando o método Object.keys , que retorna uma matriz das propriedades enumeráveis de um objeto. Para retornar um array de chaves de iterObject , passe a palavra-chave this como um argumento para Object.keys .
Por exemplo:
let objProperties = Object.keys(this)
O acesso a esta matriz permitirá que você defina o comportamento de iteração do objeto.
Em seguida, você precisa acompanhar as iterações do objeto. Você pode conseguir isso usando variáveis de contador.
Por exemplo:
let propertyIndex = 0;
let childIndex = 0;
Você usará a primeira variável de contador para controlar as propriedades do objeto e a segunda para controlar os filhos da propriedade.
Em seguida, você precisará implementar e retornar o próximo método.
Igual a:
return {
next() {
// Subsequent code blocks go here...
}
}
Dentro do próximo método, você precisará lidar com um caso extremo que ocorre quando todo o objeto foi iterado. Para lidar com o caso extremo, você deve retornar um objeto com o valor definido como indefinido e definido como true .
Veja como lidar com o caso extremo:
if (propertyIndex > objProperties.length - 1) {
return {
value: undefined,
done: true,
};
}
Em seguida, você precisará acessar as propriedades do objeto e seus elementos filhos usando as variáveis de contador declaradas anteriormente.
Igual a:
// Accessing parent and child properties
const properties = this[objProperties[propertyIndex]];
const property = properties[childIndex];
Em seguida, você precisa implementar alguma lógica para incrementar as variáveis do contador. A lógica deve redefinir o childIndex quando não houver mais elementos na matriz de uma propriedade e passar para a próxima propriedade no objeto. Além disso, ele deve incrementar childIndex , se ainda houver elementos no array da propriedade atual.
Por exemplo:
// Index incrementing logic
if (childIndex >= properties.length - 1) {
// if there are no more elements in the child array
// reset child index
childIndex = 0;
// Move to the next property
propertyIndex++;
} else {
// Move to the next element in the child array
childIndex++
}
Por fim, retorne um objeto com a propriedade done definida como false e a propriedade value definida como o elemento filho atual na iteração.
Por exemplo:
return {
done: false,
value: property,
};
Sua função Symbol.iterator concluída deve ser semelhante ao bloco de código abaixo:
iterObject[Symbol.iterator] = function () {
const objProperties = Object.keys(this);
let propertyIndex = 0;
let childIndex = 0;
return {
next: () => {
//Handling edge case
if (propertyIndex > objProperties.length - 1) {
return {
value: undefined,
done: true,
};
}
// Accessing parent and child properties
const properties = this[objProperties[propertyIndex]];
const property = properties[childIndex];
// Index incrementing logic
if (childIndex >= properties.length - 1) {
// if there are no more elements in the child array
// reset child index
childIndex = 0;
// Move to the next property
propertyIndex++;
} else {
// Move to the next element in the child array
childIndex++
}
return {
done: false,
value: property,
};
},
};
};
A execução de um loop for… of em iterObject após essa implementação não gerará um erro, pois implementa um método Symbol.iterator .
A implementação manual de iteradores, como fizemos acima, não é recomendada, pois é muito propensa a erros e a lógica pode ser difícil de gerenciar.
Geradores de JavaScript
Um gerador de JavaScript é uma função que você pode pausar e retomar sua execução a qualquer momento. Esse comportamento permite produzir uma sequência de valores ao longo do tempo.
Uma função geradora, que é uma função que retorna um Gerador, oferece uma alternativa à criação de iteradores.
Você pode criar uma função geradora da mesma forma que criaria uma declaração de função em JavaScript. A única diferença é que você deve anexar um asterisco ( * ) à palavra-chave da função.
Por exemplo:
function* example () {
return "Generator"
}
Quando você chama uma função normal em JavaScript, ela retorna o valor especificado por sua palavra-chave return ou indefinido caso contrário. Mas uma função geradora não retorna nenhum valor imediatamente. Ele retorna um objeto Generator, que você pode atribuir a uma variável.
Para acessar o valor atual do iterador, chame o próximo método no objeto Generator.
Por exemplo:
const gen = example();
console.log(gen.next()); // { value: 'Generator', done: true }
No exemplo acima, a propriedade value veio de uma palavra-chave return , encerrando efetivamente o gerador. Esse comportamento geralmente é indesejável com as funções do gerador, pois o que as distingue das funções normais é a capacidade de pausar e reiniciar a execução.
A palavra-chave de rendimento
A palavra-chave yield fornece uma maneira de iterar por meio de valores em geradores, pausando a execução de uma função geradora e retornando o valor que a segue.
Por exemplo:
function* example() {
yield "Model S"
yield "Model X"
yield "Cyber Truck"
return "Tesla"
}
const gen = example();
console.log(gen.next()); // { value: 'Model S', done: false }
No exemplo acima, quando o próximo método for chamado no gerador de exemplo , ele fará uma pausa toda vez que encontrar a palavra-chave yield . A propriedade done também será definida como false até encontrar uma palavra-chave return .
Chamando o próximo método várias vezes no gerador de exemplo para demonstrar isso, você terá o seguinte como saída.
console.log(gen.next()); // { value: 'Model X', done: false }
console.log(gen.next()); // { value: 'Cyber Truck', done: false }
console.log(gen.next()); // { value: 'Tesla', done: true }
console.log(gen.next()); // { value: undefined, done: true }
Você também pode iterar sobre um objeto Generator usando o loop for…of .
Por exemplo:
for (const iterator of gen) {
console.log(iterator);
}
/*
Model S
Model X
Cyber Truck
*/
Usando iteradores e geradores
Embora iteradores e geradores possam parecer conceitos abstratos, eles não são. Eles podem ser úteis ao trabalhar com fluxos de dados infinitos e coleções de dados. Você também pode usá-los para criar identificadores exclusivos. As bibliotecas de gerenciamento de estado, como MobX-State-Tree (MST), também as usam sob o capô.
Deixe um comentário