Pregunta 1Justo después de llamar a una función declarada con function* y obtener un generador, ¿hasta dónde se ejecutó el cuerpo?
Iteradores y generadores — Obtener valores de uno en uno
Los iteradores entregan valores de uno en uno y los generadores se pausan en cada yield. Verás next(), for...of, el spread y cómo tomar solo lo necesario de una secuencia infinita.
Cuando entregas turnos numerados uno por uno, o muestras una lista larga de reseñas de pocas en pocas, generar todos los números o páginas de antemano significa preparar algunos que nunca se usarán. Puedes escribir una función que recuerde el siguiente valor con una clausura, pero entonces tienes que llevar tú mismo la cuenta de hasta dónde llegaste y de si ya terminaste.
En este artículo verás los iteradores, que devuelven valores de uno en uno, y los generadores, que te permiten escribirlos como funciones.
Funciones que siguen donde se quedaron — generadores y yield
Supón que quieres una función que, cada vez que la llames, entregue el siguiente número de turno de una ventanilla: 101, luego 102, luego 103. Una función normal termina en cuanto ejecuta return, y la siguiente llamada vuelve a empezar desde arriba, así que necesitarías una variable de fuera para recordar hasta dónde llegaste.
Cuando llamas a una función generadora (una función declarada con function* en la que puedes usar yield), el cuerpo no se ejecuta: lo que recibes es un generador (un objeto que hace avanzar el cuerpo poco a poco). Cada vez que llamas a su next(), el cuerpo avanza hasta el siguiente yield y se pausa ahí. next() devuelve un objeto cuyo value contiene el valor del yield y cuyo done indica si el cuerpo ya terminó.
// Si la declaras con function*, es una función generadora
function* callNumbers() {
console.log("Abrimos la ventanilla");
yield 101;
yield 102;
yield 103;
}
// Al llamarla, el cuerpo no se ejecuta: solo devuelve un generador
const numbers = callNumbers();
// Cada llamada a next() avanza hasta el siguiente yield y se pausa
const first = numbers.next(); // Aquí se muestra "Abrimos la ventanilla"
console.log(`${first.value} / ${first.done}`); // 101 / false
console.log(numbers.next().value); // 102
console.log(numbers.next().value); // 103
// Cuando ya no queda ningún yield, done pasa a true
const last = numbers.next();
console.log(`${last.value} / ${last.done}`); // undefined / true
Cuando el cuerpo llega al final o ejecuta return, done pasa a true, y las llamadas a next() que vengan después ya no ejecutan el cuerpo. El propio generador lleva la cuenta de hasta dónde llegó, así que no necesitas una variable de fuera para recordar el número de turno.
No puedes escribir yield dentro de un callback
Solo puedes escribir yield dentro de las llaves de una función declarada con function*. Una función flecha que le pasas a forEach es otra función, así que escribirlo ahí da un SyntaxError, y el mensaje de error ni siquiera menciona yield. Para entregar los elementos de un array de uno en uno, escribe yield en el cuerpo de un for o un for...of.
Recorrer sin escribir next() — el protocolo de iteración
Supón que quieres mostrar en orden el estado de un envío: «Recibido», «Enviado», «En camino». Si escribes una llamada a next() por paso, necesitas una línea por cada paso y tienes que agregar más cada vez que se suma un paso. Si lo recorres con while, igual tienes que escribir tú la comprobación de done y la siguiente llamada a next().
La regla de que cada llamada a next() devuelva un objeto con value y done se llama protocolo de iteración, y un objeto que la cumple es un iterador. Los generadores de la sección anterior son iteradores. for...of sigue llamando a next() hasta que done es true y va poniendo cada value en la variable del bucle.
function* deliverySteps() {
yield "Recibido";
yield "Enviado";
yield "En camino";
}
// for...of llama a next() hasta que done sea true
for (const step of deliverySteps()) {
console.log(step); // 3 líneas: Recibido / Enviado / En camino
}
// El mismo comportamiento escrito con while y next()
const steps = deliverySteps();
let result = steps.next();
while (!result.done) {
console.log(result.value); // 3 líneas: Recibido / Enviado / En camino
result = steps.next();
}
A for...of le puedes pasar cualquier valor iterable (uno del que for...of puede obtener un iterador), como un array, un Map o un generador. A for...of no le importa de qué tipo es el valor: solo llama a next() sobre el iterador que obtiene, así que todos se recorren de la misma forma. La tabla de abajo muestra qué termina en la variable del bucle.
| Lo que le pasas a for...of | Valor de la variable en cada vuelta | Notas |
|---|---|---|
| El array ["Recibido", "Enviado"] | Cada elemento ("Recibido" → "Enviado") | Cada recorrido crea un iterador nuevo que empieza por el primer elemento |
| Map | Un array de 2 elementos [clave, valor] | Las entradas salen en el orden en que se agregaron |
| Generador | Los valores escritos en yield | Es su propio iterador, así que lo que hayas avanzado con next() se mantiene |
| Un objeto como { name: "Bolígrafo" } | Nada (TypeError: … is not iterable) | No es iterable, así que para recorrer sus claves usa for...in u Object.entries |
Convertirlo en un array — el spread y el uso único
Supón que quieres mostrar en una sola línea los horarios de recolección de paquetes, como «10:00 / 13:00 / 16:00». Un generador no es un array, así que no tiene join ni length, y por sí solo ni siquiera te deja contar sus valores. Si metes cada valor en un array con push dentro de un for...of, sumas líneas solo para eso.
Si pones un generador dentro de los corchetes de un array con el operador spread ..., saca valores hasta que done es true y los coloca en orden en un array nuevo. Una vez que escribes const slotList = [...slots];, puedes usar tanto join como length.
function* pickupSlots() {
yield "10:00";
yield "13:00";
yield "16:00";
}
// Un generador no es un array, así que no tiene length
const slots = pickupSlots();
console.log(slots.length); // undefined
// Con el spread dentro de [], los valores quedan en orden
const slotList = [...slots];
console.log(slotList.join(" / ")); // 10:00 / 13:00 / 16:00
// Si vuelves a expandir un generador ya agotado, obtienes un array vacío
console.log([...slots].length); // 0
// Si vuelves a llamar a la función, obtienes otro generador que empieza desde el principio
console.log([...pickupSlots()].length); // 3
Si necesitas los mismos valores en dos o más lugares, expándelos una vez en un array, guárdalo en una variable y reutiliza ese array. Un generador que ya recorriste hasta el final con for...of también da un array vacío si luego lo expandes.
Generar solo lo que necesitas — evaluación perezosa y secuencias infinitas
Si numeras avisos de forma correlativa empezando por 1, no sabes de antemano cuántos habrá. Si generas 1000 números en un array por adelantado, el resto se calcula aunque solo uses 3. Y si no hay un límite, el array nunca terminaría de construirse.
Con los generadores puedes escribir evaluación perezosa (calcular un valor solo en el momento en que se necesita). Incluso una función que hace yield sin fin con while (true) su cuerpo solo avanza tantas veces como llames a next(). La desestructuración de arrays también funciona con generadores y saca exactamente tantos valores como pongas del lado izquierdo.
function* serialFrom(start) {
let number = start;
while (true) {
console.log(`Se generó el ${number}`);
yield number;
number++;
}
}
// La desestructuración saca tantos valores como pongas a la izquierda
const [first, second] = serialFrom(1); // Se generó el 1 / Se generó el 2
console.log(`${first}, ${second}`); // 1, 2
// for...of deja de sacar valores en cuanto sales con break
for (const number of serialFrom(1)) {
console.log(`Aviso n.º ${number}`);
if (number === 3) break;
}
// Se generó el 1 / Aviso n.º 1 / Se generó el 2 / Aviso n.º 2
// Se generó el 3 / Aviso n.º 3 (el 4 nunca se genera)
Para convertir una secuencia infinita en un array, pon en medio otro generador que saque una cantidad fija de valores y luego haga return. Cuando ese generador intermedio termina, done pasa a true, así que el spread también se detiene ahí.
Si nunca llega a yield, next() nunca regresa
Si escribes un bucle while (true) que solo hace yield cuando se cumple una condición de if, y esa condición nunca se cumple, next() nunca regresa. La ejecución no termina y las líneas de abajo nunca se ejecutan. Cuando hagas un bucle sin fin, asegúrate primero de que tarde o temprano va a llegar un valor que cumpla la condición.
Verificación de conocimientos
Responde cada pregunta una a una.
Pregunta 2Si ejecutas [...slots] dos veces seguidas sobre un generador slots que hace yield de 3 valores, ¿qué longitud tiene la segunda vez?
Pregunta 3Con un generador gen que hace yield sin fin con while (true), ¿cuál de estas formas nunca termina?