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.
