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 o IteratorResult interface.
O IteratorResult interface compreende duas propriedades: feito e valor. O feito propriedade é um booleano que retorna
falso se o iterador pode produzir o próximo valor em sua sequência ou verdadeiro se o iterador tiver concluído sua sequência.O valor propriedade é um valor JavaScript retornado pelo iterador durante sua sequência. Quando um iterador completa sua sequência (quando feitoverdadeiro), esta propriedade retorna indefinido.
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 para de laço.
Por exemplo:
const frutas = ["Banana", "Manga", "Maçã", "Uvas"];
para (const iterador de frutas) {
console.log (iterador);
}
/*
Banana
manga
Maçã
Uvas
*/
Este exemplo itera sobre o frutas matriz usando um para de laço. 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, Conjuntos e mapas, são iteráveis integrados porque eles (ou um dos objetos em sua cadeia de protótipo) implementam um @@iterator método.
Outros tipos, como Objects, não são iteráveis por padrão.
Por exemplo:
const iterObject = {
carros: ["Tesla", "BMW", "Toyota"],
animais: ["Gato", "Cachorro", "Hamster"],
comida: ["Hambúrgueres", "Pizza", "Massa"],
};para (const iterador de iterObject) {
console.log (iterador);
}
// TypeError: iterObject não é iterável
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 Symbol.iterator método no objeto. Para se tornar iterável, esse método deve retornar um objeto que implementa o IteratorResult interface.
O Symbol.iterator símbolo tem o mesmo propósito que @@iterator e pode ser usado de forma intercambiável em "especificação", mas não em código como @@iterator não é uma sintaxe JavaScript válida.
Os blocos de código abaixo fornecem um exemplo de como tornar um objeto iterável usando o iterObject.
Primeiro, adicione o Symbol.iterator método para iterObject usando uma função declaração.
Igual a:
iterObject[Símbolo.iterator] = função () {
// Os blocos de código subsequentes vão aqui...
}
Em seguida, você precisará acessar todas as chaves no objeto que deseja tornar iterável. Você pode acessar as chaves usando o Object.keys método, que retorna uma matriz das propriedades enumeráveis de um objeto. Para retornar uma matriz de iterObject, passe as esse palavra-chave como um argumento para Object.keys.
Por exemplo:
deixar objPropriedades = Objeto.chaves(esse)
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:
deixar propertyIndex = 0;
deixar childIndex = 0;
Você usará a primeira variável de contador para acompanhar as propriedades do objeto e a segunda para acompanhar os filhos da propriedade.
Em seguida, você precisará implementar e retornar o próximo método.
Igual a:
retornar {
próximo() {
// Os blocos de código subsequentes vão aqui...
}
}
Dentro de 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 feito definido como verdadeiro.
Se este caso não for tratado, tentar iterar sobre o objeto resultará em um loop infinito.
Veja como lidar com o caso extremo:
se (índice de propriedade > objProperties.comprimento- 1) {
retornar {
valor: indefinido,
feito: verdadeiro,
};
}
Em seguida, você precisará acessar as propriedades do objeto e seus elementos filhos usando as variáveis de contador declaradas anteriormente.
Igual a:
// Acessando propriedades pai e filho
const propriedades = esse[objProperties[propertyIndex]];
const propriedade = propriedades[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, deve incrementar childIndex, se ainda houver elementos na matriz da propriedade atual.
Por exemplo:
// Lógica de incremento do índice
if (childIndex >= properties.length - 1) {
// se não houver mais elementos no array filho
// reiniciarcriançaíndice
índicefilho = 0;
// Move para a próxima propriedade
índice de propriedade++;
} outro {
// Move para o próximo elemento no array filho
childIndex++
}
Finalmente, devolva um objeto com o feito propriedade definida como falso e a valor propriedade definida para o elemento filho atual na iteração.
Por exemplo:
retornar {
feito: falso,
valor: propriedade,
};
seu concluído Symbol.iterator função deve ser semelhante ao bloco de código abaixo:
iterObject[Símbolo.iterator] = função () {
const objPropriedades = Objeto.chaves(esse);
deixar propertyIndex = 0;
deixar childIndex = 0;retornar {
próximo: () => {
// Tratando casos extremos
se (índice de propriedade > objProperties.comprimento- 1) {
retornar {
valor: indefinido,
feito: verdadeiro,
};
}// Acessando propriedades pai e filho
const propriedades = esse[objProperties[propertyIndex]];
const propriedade = propriedades[childIndex];// Lógica de incremento do índice
if (childIndex >= properties.length - 1) {
// se não houver mais elementos no array filho
// reiniciarcriançaíndice
índicefilho = 0;
// Move para a próxima propriedade
índice de propriedade++;
} outro {
// Move para o próximo elemento no array filho
childIndex++
}
retornar {
feito: falso,
valor: propriedade,
};
},
};
};
executando um para de loop em iterObject após esta implementação não lançará um erro, pois implementa um Symbol.iterator método.
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 acrescentar um asterisco (*) à palavra-chave da função.
Por exemplo:
função* exemplo () {
retornar"Gerador"
}
Quando você chama uma função normal em JavaScript, ela retorna o valor especificado por seu retornar palavra-chave ou indefinido de outra forma. 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 = exemplo();
console.log (gen.next()); // { valor: 'Gerador', feito: verdadeiro }
No exemplo acima, o valor propriedade veio de um retornar palavra-chave, 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
O colheita A palavra-chave 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:
função* exemplo() {
colheita"Modelo S"
colheita"Modelo X"
colheita"Caminhão Cibernético"retornar"Tesla"
}const gen = exemplo();
console.log (gen.next()); // { valor: 'Modelo S', feito: falso }
No exemplo acima, quando o próximo método é chamado no exemplo gerador, ele fará uma pausa toda vez que encontrar o colheita palavra-chave. O feito propriedade também será definida como falso até que encontra um retornar palavra-chave.
Chamando o próximo método várias vezes no exemplo gerador para demonstrar isso, você terá o seguinte como sua saída.
console.log (gen.next()); // { valor: 'Modelo X', feito: falso }
console.log (gen.next()); // { valor: 'Caminhão Cibernético', feito: falso }
console.log (gen.next()); // { valor: 'Tesla', feito: verdadeiro }
console.log (gen.next()); // { valor: indefinido, feito: verdadeiro }
Você também pode iterar sobre um objeto Generator usando o para de laço.
Por exemplo:
para (const iterador de geração) {
console.log (iterador);
}
/*
Modelo S
Modelo X
caminhão cibernético
*/
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 os usam sob o capô.