Generators y yield en JavaScript: funciones pausables e iteración controlada

Los generators son funciones especiales de JavaScript que pueden pausar su ejecución y reanudarla más tarde, manteniendo su estado entre llamadas. Se definen con function* y usan yield para devolver valores de forma perezosa. Son la base de los protocolos de iteración y de las funciones asíncronas de JavaScript.

function* y yield: la mecánica básica

Llamar a una función generadora no ejecuta su cuerpo: devuelve un objeto iterador. Cada vez que llamas a .next(), el generador ejecuta hasta el siguiente yield y se pausa:

function* contador() {
  console.log('Inicio del generador');
  yield 1;
  console.log('Después del primer yield');
  yield 2;
  console.log('Después del segundo yield');
  yield 3;
  console.log('Generador terminado');
}

const gen = contador();  // Crea el iterador, no ejecuta nada

let resultado = gen.next();
// Imprime: 'Inicio del generador'
console.log(resultado);  // { value: 1, done: false }

resultado = gen.next();
// Imprime: 'Después del primer yield'
console.log(resultado);  // { value: 2, done: false }

resultado = gen.next();
// Imprime: 'Después del segundo yield'
console.log(resultado);  // { value: 3, done: false }

resultado = gen.next();
// Imprime: 'Generador terminado'
console.log(resultado);  // { value: undefined, done: true }

Secuencias infinitas sin bloqueo

Los generadores pueden producir secuencias infinitas de forma segura porque solo calculan el siguiente valor cuando se les pide:

function* numeros() {
  let n = 0;
  while (true) {  // Bucle infinito, pero seguro
    yield n++;
  }
}

function* fibonacci() {
  let [a, b] = [0, 1];
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}

// Tomar solo los primeros N elementos
function tomar(gen, n) {
  const resultado = [];
  for (const valor of gen) {
    resultado.push(valor);
    if (resultado.length === n) break;
  }
  return resultado;
}

console.log(tomar(numeros(), 5));    // [0, 1, 2, 3, 4]
console.log(tomar(fibonacci(), 8)); // [0, 1, 1, 2, 3, 5, 8, 13]

Comunicación bidireccional con next(valor)

Puedes pasar un valor al generador con next(valor). Este valor se convierte en el resultado de la expresión yield dentro del generador:

function* calculadora() {
  let resultado = 0;
  while (true) {
    const entrada = yield resultado;  // yield devuelve resultado Y recibe entrada
    if (entrada === null) break;
    resultado += entrada;
  }
  return resultado;
}

const calc = calculadora();
calc.next();        // Inicia el generador (primer yield, resultado=0)
calc.next(10);      // entrada=10, resultado=10, { value: 10, done: false }
calc.next(25);      // entrada=25, resultado=35, { value: 35, done: false }
calc.next(5);       // entrada=5,  resultado=40, { value: 40, done: false }

const final = calc.next(null);  // Rompe el bucle
console.log(final); // { value: 40, done: true }

Delegación con yield*

yield* delega la iteración a otro generador o iterable, reenviando todos sus valores:

function* gen1() {
  yield 1;
  yield 2;
}

function* gen2() {
  yield 'a';
  yield* gen1();  // Delega: produce 1, 2
  yield 'b';
}

console.log([...gen2()]);  // ['a', 1, 2, 'b']

// Aplanar arrays de forma perezosa:
function* aplanar(arr) {
  for (const item of arr) {
    if (Array.isArray(item)) {
      yield* aplanar(item);  // Recursión con delegación
    } else {
      yield item;
    }
  }
}

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

Generadores asíncronos con async function*

Los generadores asíncronos combinan async con function*, permitiendo yield en contextos asíncronos y el consumo con for await...of:

async function* paginas(url) {
  let pagina = 1;
  let hayMas = true;

  while (hayMas) {
    const respuesta = await fetch(`${url}?page=${pagina}`);
    const { datos, total, porPagina } = await respuesta.json();

    yield datos;

    hayMas = pagina * porPagina < total;
    pagina++;
  }
}

// Consumir con for await...of
async function procesarTodos() {
  let total = 0;
  for await (const pagina of paginas('/api/productos')) {
    total += pagina.length;
    console.log(`Procesados ${total} productos`);
  }
}

Los errores más frecuentes al empezar con generators: llamar a la función generadora y esperar que se ejecute inmediatamente (hay que llamar a .next()), o iterar con for...of y luego intentar seguir usando el mismo iterador (ya está agotado). Los generators tampoco se pueden reiniciar: hay que crear uno nuevo cada vez.

COMPARTE ESTE ARTÍCULO

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