Protocolo iterador avanzado en JavaScript: return(), throw(), iteradores lazy e infinitos

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.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP