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.
