Como usar geradores e iteradores em JavaScript

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

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *