Generators e iterators en JavaScript: function*, Symbol.iterator y async generators

Los generadores son funciones que pueden pausar su ejecución y reanudarla más tarde, produciendo valores bajo demanda en lugar de calcularlos todos de golpe. Junto con los iteradores y el protocolo iterable de JavaScript, permiten construir abstracciones que procesan datos de forma perezosa (lazy) y con control total del flujo de ejecución.

function* y yield: pausar y reanudar

Una función generadora se declara con function* y utiliza yield para emitir valores uno a uno. Cada llamada a .next() reanuda la ejecución hasta el siguiente yield.

function* contador(inicio = 0) {
  let n = inicio;
  while (true) {
    yield n++;
  }
}

const gen = contador(1);
console.log(gen.next().value); // 1
console.log(gen.next().value); // 2
console.log(gen.next().value); // 3
// La secuencia es infinita pero solo calcula el valor cuando se pide

El objeto devuelto por .next() tiene la forma { value, done }. Cuando el generador alcanza el final o un return, done pasa a ser true.

El protocolo iterable con Symbol.iterator

Cualquier objeto puede volverse iterable implementando el método [Symbol.iterator] que devuelve un iterador con .next(). Los generadores implementan este protocolo automáticamente, lo que los hace compatibles con for...of, spread y destructuring.

function* rango(inicio, fin, paso = 1) {
  for (let i = inicio; i <= fin; i += paso) {
    yield i;
  }
}

// for...of
for (const n of rango(1, 5)) {
  console.log(n); // 1, 2, 3, 4, 5
}

// Spread
console.log([...rango(0, 10, 2)]); // [0, 2, 4, 6, 8, 10]

// Destructuring
const [a, b, c] = rango(10, 20);
console.log(a, b, c); // 10, 11, 12

Composición de generadores con yield*

yield* delega a otro iterable, permitiendo componer generadores sin aplanar manualmente los valores:

function* aplanar(arr) {
  for (const item of arr) {
    if (Array.isArray(item)) {
      yield* aplanar(item); // recursivo
    } else {
      yield item;
    }
  }
}

console.log([...aplanar([1, [2, [3, [4]]], 5])]); // [1, 2, 3, 4, 5]

Async generators para paginación lazy

Los generadores asíncronos (async function*) combinan yield con await, lo que permite generar valores que provienen de operaciones asíncronas. El caso de uso más claro es la paginación de APIs: en lugar de cargar todas las páginas de golpe, se obtienen bajo demanda.

async function* paginarAPI(url) {
  let nextUrl = url;

  while (nextUrl) {
    const res = await fetch(nextUrl);
    const { data, next } = await res.json();

    for (const item of data) {
      yield item;
    }

    nextUrl = next; // null cuando no hay más páginas
  }
}

// Consumir con for await...of
for await (const articulo of paginarAPI('/api/articulos?page=1')) {
  console.log(articulo.titulo);
  // Cada página se carga solo cuando se necesita
}

Pipeline de transformación de datos

Los generadores encadenados forman pipelines de transformación que procesan elementos uno a uno sin crear arrays intermedios en memoria:

function* map(iterable, fn) {
  for (const item of iterable) yield fn(item);
}

function* filter(iterable, pred) {
  for (const item of iterable) {
    if (pred(item)) yield item;
  }
}

function* take(iterable, n) {
  let count = 0;
  for (const item of iterable) {
    if (count++ >= n) return;
    yield item;
  }
}

// Pipeline: números naturales ? cuadrados ? pares ? primeros 5
const resultado = take(
  filter(
    map(contador(1), x => x * x),
    x => x % 2 === 0
  ),
  5
);

console.log([...resultado]); // [4, 16, 36, 64, 100]

Este pipeline es completamente lazy: no calcula ningún cuadrado hasta que el consumidor pide un elemento, y se detiene en cuanto se recogen los 5 resultados. Es equivalente a la composición de iteradores de Python o las secuencias de Kotlin, pero en JavaScript puro.

COMPARTE ESTE ARTÍCULO

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