Iteradores e o protocolo Symbol.iterator

1. O que são Iteradores e o Protocolo de Iteração

Iteradores são objetos que implementam o protocolo de iteração do JavaScript, fornecendo um mecanismo padronizado para percorrer sequências de dados. Um iterador é essencialmente um objeto com um método next() que retorna um objeto com duas propriedades: value (o próximo valor na sequência) e done (booleano indicando se a sequência terminou).

O protocolo de iteração distingue dois conceitos fundamentais:

  • Iterable: um objeto que implementa o método [Symbol.iterator](), retornando um iterador
  • Iterator: o objeto retornado por [Symbol.iterator](), que implementa o método next()

Symbol.iterator é uma chave simbólica padrão do JavaScript que permite que objetos sejam iteráveis. É o que torna possível usar for...of, operador spread (...), e Array.from() em objetos personalizados.

// Estrutura básica de um iterador
const iteradorBasico = {
  contador: 0,
  next() {
    if (this.contador < 3) {
      return { value: this.contador++, done: false };
    }
    return { value: undefined, done: true };
  }
};

console.log(iteradorBasico.next()); // { value: 0, done: false }
console.log(iteradorBasico.next()); // { value: 1, done: false }
console.log(iteradorBasico.next()); // { value: 2, done: false }
console.log(iteradorBasico.next()); // { value: undefined, done: true }

2. Criando Iteradores Personalizados

Podemos criar iteradores mais sofisticados que implementam não apenas next(), mas também return() e throw() para gerenciamento de ciclo de vida.

function criarIteradorRange(inicio, fim, passo = 1) {
  let valorAtual = inicio;

  return {
    next() {
      if (valorAtual <= fim) {
        const valor = valorAtual;
        valorAtual += passo;
        return { value: valor, done: false };
      }
      return { value: undefined, done: true };
    },
    return() {
      console.log('Iterador finalizado prematuramente');
      return { value: undefined, done: true };
    },
    throw(erro) {
      console.error('Erro no iterador:', erro);
      return { value: undefined, done: true };
    }
  };
}

const iterador = criarIteradorRange(1, 5, 2);
console.log(iterador.next()); // { value: 1, done: false }
console.log(iterador.next()); // { value: 3, done: false }
console.log(iterador.next()); // { value: 5, done: false }
console.log(iterador.next()); // { value: undefined, done: true }

3. Tornando Objetos Iteráveis com Symbol.iterator

Para tornar um objeto comum iterável, precisamos implementar o método [Symbol.iterator]. Isso permite que o objeto seja usado com for...of e outras construções que esperam iteráveis.

const catalogoProdutos = {
  produtos: [
    { id: 1, nome: 'Notebook', preco: 4500 },
    { id: 2, nome: 'Mouse', preco: 150 },
    { id: 3, nome: 'Teclado', preco: 300 }
  ],
  [Symbol.iterator]() {
    let indice = 0;
    const produtos = this.produtos;

    return {
      next() {
        if (indice < produtos.length) {
          return { value: produtos[indice++], done: false };
        }
        return { value: undefined, done: true };
      }
    };
  }
};

// Usando for...of
for (const produto of catalogoProdutos) {
  console.log(`${produto.nome}: R$${produto.preco}`);
}

// vs loop tradicional for...in (não funciona com iteradores)
// for...in itera sobre propriedades enumeráveis, não sobre o protocolo de iteração

4. Consumindo Iteradores no Node.js

No Node.js, iteradores são particularmente úteis para processar streams de dados e arquivos grandes de forma eficiente.

const fs = require('fs');
const readline = require('readline');

// Iteração assíncrona com for await...of
async function processarArquivoLog(caminhoArquivo) {
  const streamLeitura = fs.createReadStream(caminhoArquivo);
  const rl = readline.createInterface({
    input: streamLeitura,
    crlfDelay: Infinity
  });

  let totalLinhas = 0;
  let erros = 0;

  for await (const linha of rl) {
    totalLinhas++;
    if (linha.includes('ERROR')) {
      erros++;
      console.log(`Erro encontrado: ${linha}`);
    }
  }

  console.log(`Total de linhas: ${totalLinhas}, Erros: ${erros}`);
}

// Exemplo de iteração sobre dados paginados de API
async function iterarDadosPaginados(urlBase, maxPaginas = 5) {
  const iterador = {
    paginaAtual: 1,
    [Symbol.asyncIterator]() {
      return {
        next: async () => {
          if (this.paginaAtual > maxPaginas) {
            return { done: true, value: undefined };
          }

          const resposta = await fetch(`${urlBase}?page=${this.paginaAtual}`);
          const dados = await resposta.json();
          this.paginaAtual++;

          return { value: dados, done: false };
        }
      };
    }
  };

  for await (const pagina of iterador) {
    console.log(`Processando página com ${pagina.length} itens`);
  }
}

5. Iteradores no Ecossistema React

Em React, iteradores podem ser combinados com hooks como useMemo para otimizar renderizações de listas.

import React, { useMemo, useState } from 'react';

function ListaFiltrada({ itens, filtro }) {
  // Criando um iterador personalizado para filtrar itens
  const iteradorFiltrado = useMemo(() => {
    return {
      itens,
      filtro,
      [Symbol.iterator]() {
        let indice = 0;
        const itensFiltrados = this.itens.filter(item => 
          item.nome.toLowerCase().includes(this.filtro.toLowerCase())
        );

        return {
          next() {
            if (indice < itensFiltrados.length) {
              return { value: itensFiltrados[indice++], done: false };
            }
            return { value: undefined, done: true };
          }
        };
      }
    };
  }, [itens, filtro]);

  // Convertendo o iterador para array para renderização
  const itensParaRenderizar = useMemo(() => {
    return Array.from(iteradorFiltrado);
  }, [iteradorFiltrado]);

  return (
    <ul>
      {itensParaRenderizar.map(item => (
        <li key={item.id}>{item.nome} - R${item.preco}</li>
      ))}
    </ul>
  );
}

// Componente pai
function Catalogo() {
  const [filtro, setFiltro] = useState('');
  const produtos = [
    { id: 1, nome: 'Notebook Dell', preco: 4500 },
    { id: 2, nome: 'Mouse Logitech', preco: 150 },
    { id: 3, nome: 'Teclado Mecânico', preco: 300 }
  ];

  return (
    <div>
      <input 
        type="text" 
        value={filtro} 
        onChange={e => setFiltro(e.target.value)} 
        placeholder="Filtrar produtos..."
      />
      <ListaFiltrada itens={produtos} filtro={filtro} />
    </div>
  );
}

6. Integração com Generators e Coleções Nativas

Generators (function*) são fábricas de iteradores que simplificam a criação de sequências complexas.

// Generator como fábrica de iteradores
function* geradorSequencia(inicio, fim) {
  for (let i = inicio; i <= fim; i++) {
    yield i;
  }
}

// Map, Set e Arrays já implementam Symbol.iterator
const mapa = new Map([
  ['a', 1],
  ['b', 2],
  ['c', 3]
]);

const conjunto = new Set([1, 2, 3, 4, 5]);

// Combinando iteradores com Array.from() e operador spread
const sequencia = [...geradorSequencia(1, 5)];
console.log(sequencia); // [1, 2, 3, 4, 5]

const paresDoMapa = Array.from(mapa);
console.log(paresDoMapa); // [['a', 1], ['b', 2], ['c', 3]]

// Combinando múltiplos iteradores
function* combinadorIteradores(...iteraveis) {
  for (const iteravel of iteraveis) {
    yield* iteravel;
  }
}

const combinado = [...combinadorIteradores(
  geradorSequencia(1, 3),
  [10, 20, 30],
  new Set(['x', 'y', 'z'])
)];
console.log(combinado); // [1, 2, 3, 10, 20, 30, 'x', 'y', 'z']

7. Boas Práticas e Casos de Uso Avançados

Iteradores infinitos permitem lazy evaluation, processando dados sob demanda sem consumir memória desnecessariamente.

// Iterador infinito com lazy evaluation
function* geradorFibonacci() {
  let a = 0, b = 1;
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}

// Encadeamento de iteradores para pipelines de dados
function* filtrar(iteravel, predicate) {
  for (const item of iteravel) {
    if (predicate(item)) yield item;
  }
}

function* mapear(iteravel, transform) {
  for (const item of iteravel) {
    yield transform(item);
  }
}

function* limitar(iteravel, maximo) {
  let contador = 0;
  for (const item of iteravel) {
    if (contador >= maximo) return;
    yield item;
    contador++;
  }
}

// Pipeline de processamento
const fibonacci = geradorFibonacci();
const pipeline = limitar(
  mapear(
    filtrar(fibonacci, n => n % 2 === 0),
    n => n * 10
  ),
  5
);

console.log([...pipeline]); // [0, 20, 80, 340, 1440]

// Cuidados com mutação durante iteração
const itensFrageis = new WeakMap();
const dados = { id: 1, valor: 'importante' };
itensFrageis.set(dados, { processado: false });

// Iterador seguro que não mantém referências fortes
function* iteradorSeguro(weakMap) {
  // Implementação segura para evitar vazamentos de memória
}

Boas Práticas

  1. Sempre implemente return() em iteradores que gerenciam recursos (arquivos, conexões)
  2. Prefira generators para criar iteradores complexos - são mais legíveis e seguros
  3. Use lazy evaluation com iteradores infinitos para economizar memória
  4. Combine iteradores para criar pipelines de processamento de dados
  5. Cuidado com mutações durante iteração - considere usar WeakMap para metadados

Referências