El protocolo iterador de JavaScript define cómo los objetos exponen secuencias de valores para su consumo con for...of, spread y desestructuración. Más allá del uso básico, los métodos return() y throw() del protocolo permiten gestionar la limpieza de recursos y la propagación de errores, los iteradores lazy e infinitos generan valores bajo demanda, y los Iterator Helpers de ES2025 encadenan transformaciones sin crear arrays intermedios.
El protocolo iterador completo
Un iterador cumple el protocolo cuando expone un método next() que devuelve { value, done }. Pero el protocolo también define dos métodos opcionales: return() para liberación anticipada de recursos y throw() para inyectar errores:
class RecursoConLimpieza {
#abierto = false;
#datos;
constructor(datos) {
this.#datos = datos;
this.#abierto = true;
console.log('Recurso abierto');
}
[Symbol.iterator]() {
let indice = 0;
const datos = this.#datos;
const cerrar = () => {
if (this.#abierto) {
this.#abierto = false;
console.log('Recurso cerrado');
}
};
return {
next: () => {
if (!this.#abierto) return { value: undefined, done: true };
if (indice >= datos.length) {
cerrar();
return { value: undefined, done: true };
}
return { value: datos[indice++], done: false };
},
// Llamado cuando for...of termina antes de llegar al final (break, return, throw)
return: (valor) => {
cerrar(); // Limpiar el recurso aunque no hayamos llegado al final
return { value: valor, done: true };
},
// Llamado si se inyecta un error desde fuera
throw: (error) => {
cerrar();
throw error;
},
};
}
}
const recurso = new RecursoConLimpieza([1, 2, 3, 4, 5]);
for (const valor of recurso) {
if (valor === 3) break; // return() se llama automáticamente
console.log(valor);
}
// "Recurso abierto", 1, 2, "Recurso cerrado" limpieza garantizada
return() y throw() en generadores
Los generadores implementan return() y throw() de forma nativa. Se pueden llamar externamente para controlar la ejecución del generador:
function* generadorConLimpieza() {
console.log('Inicio del generador');
try {
yield 1;
yield 2;
yield 3;
} finally {
// El bloque finally se ejecuta aunque se llame a return() externamente
console.log('Limpieza del generador');
}
console.log('Este código nunca se ejecuta si se llama return()');
}
const gen = generadorConLimpieza();
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.return(99)); // Limpieza del generador / { value: 99, done: true }
console.log(gen.next()); // { value: undefined, done: true }
// throw() inyecta un error en el punto de yield actual
function* generadorErrores() {
try {
const valor = yield 'esperando';
console.log('Recibido:', valor);
} catch (err) {
console.log('Error capturado:', err.message);
yield 'recuperado';
}
}
const gen2 = generadorErrores();
gen2.next(); // Avanza al primer yield
gen2.throw(new Error('Fallo')); // Error capturado: Fallo
// { value: 'recuperado', done: false }
Iteradores lazy: Fibonacci infinito
Los generadores son la forma idiomática de crear iteradores lazy que producen valores bajo demanda, sin calcular ni almacenar toda la secuencia de antemano:
// Fibonacci infinito nunca acaba, consume O(1) memoria
function* fibonacci() {
let [a, b] = [0, 1];
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
// Tomar los primeros N elementos
function tomar(iterador, n) {
const resultado = [];
for (const valor of iterador) {
resultado.push(valor);
if (resultado.length >= n) break;
}
return resultado;
}
console.log(tomar(fibonacci(), 10));
// [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
// Números primos infinitos (Criba de Eratóstenes lazy)
function* primos() {
const compuestos = new Set();
let n = 2;
while (true) {
if (!compuestos.has(n)) {
yield n;
// Marcar múltiplos como compuestos (lazy solo hasta donde llegamos)
for (let m = n * n; m < n * n + n * 100; m += n) {
compuestos.add(m);
}
}
n++;
}
}
console.log(tomar(primos(), 10));
// [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
Encadenamiento sin arrays intermedios
// Sin generadores: crea 3 arrays temporales
const resultado = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
.filter(n => n % 2 === 0) // Array temporal [2, 4, 6, 8, 10]
.map(n => n * n) // Array temporal [4, 16, 36, 64, 100]
.slice(0, 3); // Array final [4, 16, 36]
// Con generadores: procesamiento lazy, sin arrays intermedios
function* filtrar(iter, pred) {
for (const v of iter) if (pred(v)) yield v;
}
function* mapear(iter, fn) {
for (const v of iter) yield fn(v);
}
function* limitar(iter, n) {
for (const v of iter) {
yield v;
if (--n === 0) return;
}
}
const resultadoLazy = [
...limitar(
mapear(
filtrar([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], n => n % 2 === 0),
n => n * n
),
3
)
];
// [4, 16, 36] mismos valores, sin arrays temporales
Iterator Helpers (ES2025)
La propuesta Iterator Helpers añade métodos directamente al prototipo de los iteradores, eliminando la necesidad de implementar las funciones de filtro/mapa lazy manualmente:
// Con Iterator Helpers (Chrome 122+, Node.js 22+) const resultado2 = fibonacci() .filter(n => n % 2 === 0) // Solo pares de Fibonacci .map(n => n ** 2) // Elevar al cuadrado .take(5) // Solo los 5 primeros .toArray(); // Materializar en array console.log(resultado2); // [0, 4, 64, 1156, 17424] // drop: saltar los N primeros elementos const sinPrimeros = fibonacci() .drop(5) .take(5) .toArray(); console.log(sinPrimeros); // [5, 8, 13, 21, 34] // flatMap: aplanar iteradores anidados const iterPares = Iterator.from([1, 2, 3]) .flatMap(n => [n, n * 10].values()) .toArray(); console.log(iterPares); // [1, 10, 2, 20, 3, 30] // reduce y forEach también disponibles const sumaFib = fibonacci() .take(10) .reduce((acc, n) => acc + n, 0); console.log(sumaFib); // 88
El protocolo iterador avanzado es especialmente valioso cuando se trabaja con fuentes de datos de tamaño desconocido o potencialmente infinito: streams de eventos, paginación de APIs, generación procedural de contenido o procesamiento de archivos grandes. Los Iterator Helpers de ES2025 hacen que este código sea tan expresivo como la cadena de métodos de array, pero sin el coste en memoria de los arrays intermedios.
