Generadores e Iteradores
Funciones generadoras, el protocolo de iteración e iterables personalizados
Iteradores e Iterables
El protocolo de iteración define una forma estándar de producir una secuencia de valores.
Un iterable es cualquier objeto que puede ser iterado (como arrays, strings, Maps).
Comprender estos protocolos te permite crear estructuras de datos personalizadas que funcionen con bucles for...of.
Conceptos Clave
- Iterable — Objeto con método [Symbol.iterator]
- Iterador — Objeto con método next() que retorna {value, done}
- Generador — Función que puede pausar y reanudar ejecución
- yield — Pausa el generador y retorna un valor
El Protocolo de Iteración
// Iterador manual
const array = [1, 2, 3];
const iterador = array[Symbol.iterator]();
console.log(iterador.next()); // { value: 1, done: false }
console.log(iterador.next()); // { value: 2, done: false }
console.log(iterador.next()); // { value: 3, done: false }
console.log(iterador.next()); // { value: undefined, done: true }
// for...of usa este protocolo internamente
for (const valor of array) {
console.log(valor); // 1, 2, 3
}
Creando Iterables Personalizados
// Objeto implementando el protocolo iterable
const rango = {
inicio: 1,
fin: 5,
// Hacerlo iterable
[Symbol.iterator]() {
let actual = this.inicio;
const fin = this.fin;
// Retornar un objeto iterador
return {
next() {
if (actual <= fin) {
return { value: actual++, done: false };
}
return { value: undefined, done: true };
}
};
}
};
// Ahora funciona con for...of
for (const num of rango) {
console.log(num); // 1, 2, 3, 4, 5
}
// Y operador spread
console.log([...rango]); // [1, 2, 3, 4, 5]
Funciones Generadoras
Los generadores son funciones que pueden pausar ejecución y producir múltiples valores:
// Sintaxis de función generadora (nota el *)
function* generadorNumeros() {
yield 1;
yield 2;
yield 3;
}
const gen = generadorNumeros();
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next()); // { value: undefined, done: true }
// Los generadores son iterables
for (const num of generadorNumeros()) {
console.log(num); // 1, 2, 3
}
console.log([...generadorNumeros()]); // [1, 2, 3]
Ejemplos Prácticos de Generadores
// Secuencia infinita
function* secuenciaInfinita() {
let i = 0;
while (true) {
yield i++;
}
}
const secuencia = secuenciaInfinita();
console.log(secuencia.next().value); // 0
console.log(secuencia.next().value); // 1
console.log(secuencia.next().value); // 2
// ...continúa para siempre
// Generador de rango (más simple que iterador manual)
function* rango(inicio, fin, paso = 1) {
for (let i = inicio; i <= fin; i += paso) {
yield i;
}
}
console.log([...rango(1, 5)]); // [1, 2, 3, 4, 5]
console.log([...rango(0, 10, 2)]); // [0, 2, 4, 6, 8, 10]
// Generador de IDs
function* generadorIDs(prefijo = "id") {
let id = 1;
while (true) {
yield `${prefijo}_${id++}`;
}
}
const idsUsuario = generadorIDs("user");
console.log(idsUsuario.next().value); // "user_1"
console.log(idsUsuario.next().value); // "user_2"
yield* para Delegación
// Delegar a otro iterable
function* concat(...iterables) {
for (const iterable of iterables) {
yield* iterable;
}
}
console.log([...concat([1, 2], [3, 4], [5])]); // [1, 2, 3, 4, 5]
// Generador recursivo (recorrido de árbol)
function* recorrerArbol(nodo) {
yield nodo.valor;
if (nodo.hijos) {
for (const hijo of nodo.hijos) {
yield* recorrerArbol(hijo);
}
}
}
const arbol = {
valor: 1,
hijos: [
{ valor: 2, hijos: [{ valor: 4 }, { valor: 5 }] },
{ valor: 3, hijos: [{ valor: 6 }] }
]
};
console.log([...recorrerArbol(arbol)]); // [1, 2, 4, 5, 3, 6]
Comunicación Bidireccional
// Pasar valores DENTRO de un generador
function* calculadora() {
const a = yield "Ingresa primer número";
const b = yield "Ingresa segundo número";
return a + b;
}
const calc = calculadora();
console.log(calc.next()); // { value: "Ingresa primer número", done: false }
console.log(calc.next(10)); // { value: "Ingresa segundo número", done: false }
console.log(calc.next(5)); // { value: 15, done: true }
// Generador con acumulador
function* totalAcumulado() {
let total = 0;
while (true) {
const valor = yield total;
total += valor;
}
}
const totales = totalAcumulado();
console.log(totales.next().value); // 0
console.log(totales.next(10).value); // 10
console.log(totales.next(5).value); // 15
console.log(totales.next(3).value); // 18
Manejo de Errores en Generadores
function* generadorConErrores() {
try {
yield 1;
yield 2;
yield 3;
} catch (error) {
console.log("Capturado:", error.message);
yield "recuperado";
}
}
const gen = generadorConErrores();
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.throw(new Error("¡Ups!")));
// Capturado: ¡Ups!
// { value: "recuperado", done: false }
// Generador con return()
const gen2 = generadorConErrores();
gen2.next(); // { value: 1, done: false }
gen2.return("salida anticipada"); // { value: "salida anticipada", done: true }
Generadores Asíncronos
// Generador async para API paginada
async function* fetchPaginas(url) {
let pagina = 1;
let hayMas = true;
while (hayMas) {
const response = await fetch(`${url}?page=${pagina}`);
const data = await response.json();
yield data.items;
hayMas = data.hasNextPage;
pagina++;
}
}
// Usar con for await...of
async function obtenerTodosLosItems() {
const todosLosItems = [];
for await (const items of fetchPaginas("/api/items")) {
todosLosItems.push(...items);
}
return todosLosItems;
}
// Generador async para streaming
async function* leerLineas(stream) {
const reader = stream.getReader();
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += new TextDecoder().decode(value);
const lineas = buffer.split("\n");
buffer = lineas.pop(); // Mantener línea incompleta
for (const linea of lineas) {
yield linea;
}
}
if (buffer) yield buffer;
}
💡 Puntos Clave
- • Los iterables implementan [Symbol.iterator] y funcionan con for...of
- • Los generadores son la forma más fácil de crear iteradores
- • Usa
yieldpara pausar yyield*para delegar - • Los generadores pueden recibir valores via next(value)
- • Generadores async con
for await...ofpara secuencias asíncronas - • Excelentes para secuencias infinitas, evaluación perezosa y streaming de datos
Ideas de práctica
- • Construye un generador infinito de Fibonacci y limita el consumo con
take(n). - • Envuelve una API paginada en un generador async y mide memoria vs obtener todo de una vez.
- • Crea un iterable personalizado que divida un string en palabras; verifica que funciona con
[...iterable]. - • Usa
yield*para componer múltiples generadores (ej., anteponer defaults, luego datos de usuario).