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étodonext()
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
- Sempre implemente
return()em iteradores que gerenciam recursos (arquivos, conexões) - Prefira generators para criar iteradores complexos - são mais legíveis e seguros
- Use lazy evaluation com iteradores infinitos para economizar memória
- Combine iteradores para criar pipelines de processamento de dados
- Cuidado com mutações durante iteração - considere usar
WeakMappara metadados
Referências
- MDN Web Docs: Iteration protocols — Documentação oficial sobre os protocolos de iteração, incluindo exemplos detalhados de implementação
- JavaScript.info: Iterables — Tutorial completo sobre iteráveis e Symbol.iterator com exemplos práticos
- Node.js Documentation: Streams — Documentação oficial sobre streams no Node.js, que utilizam o protocolo de iteração
- React Documentation: Lists and Keys — Guia oficial sobre renderização de listas no React, relacionado ao uso de iteradores
- Exploring JS: Iterators and Generators — Capítulo completo do livro "Exploring ES6" sobre iteradores e generators
- 2ality: ES6 Iterables and Iterators — Artigo técnico detalhado sobre o protocolo de iteração no ECMAScript 6