Aprende leyendo en orden

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
Cada next() mueve hacia adelante el punto de pausa
numbers =callNumbers()No se ejecutaninguna líneadel cuerpo1.ernext()Muestra el textose pausa enyield 1012.º y 3.ernext()Se pausa enyield 102y luego en 1034.ºnext()value: undefineddone: true
«Abrimos la ventanilla» se muestra en el primer next(), que luego se pausa en yield 101. Cada next() avanza hasta el siguiente yield.

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.

Entrega tantos turnos numerados como tengas preparados. ticketCount ya está declarado.

① Convierte issueTickets en una función generadora que entregue del 1 a count.

② Al terminar de entregarlos, devuelve con return «Se cerró la entrega de turnos».

③ Pasa ticketCount y obtén un generador.

④ Saca el siguiente valor 4 veces y muestra value y done con la forma «1 / false».

(Si se ejecuta correctamente, aparecerá una explicación.)

Editor JavaScript / TypeScript

Ejecutar el código para ver el resultado

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();
}
Lo que for...of hace por ti
Recorrer confor...ofLlama a next()automáticamenteComprueba doneautomáticamentePone value en stepy corre el cuerpoRecorrer túcon whileresult =steps.next()Compruebas tú!result.doneUsas result.valueen el cuerpo
Los dos repiten las columnas 2 a 4 hasta que done es true. Con for...of no escribes next() ni la comprobación de done.

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...ofValor de la variable en cada vueltaNotas
El array ["Recibido", "Enviado"]Cada elemento ("Recibido" → "Enviado")Cada recorrido crea un iterador nuevo que empieza por el primer elemento
MapUn array de 2 elementos [clave, valor]Las entradas salen en el orden en que se agregaron
GeneradorLos valores escritos en yieldEs 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

Divide las reseñas de un producto en páginas de 2 y muéstralas. reviews ya está declarado.

① En paginate, entrega cada página como un array, en orden desde el principio.

② Recorre las páginas de reviews, con 2 reseñas por página.

③ En cada página, muestra «Página 1: » seguido de los nombres de quienes escribieron las reseñas, unidos con «, ».

④ Al final, muestra la cantidad de páginas con la forma «Total: 3 páginas».

Editor JavaScript / TypeScript

Ejecutar el código para ver el resultado

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
El segundo spread da un array vacío
1.er[...slots]Los 3 horariosde 10:00 a 16:002.º[...slots]Array vacíolength es 0Nueva llamada[...pickupSlots()]Lee desdeel principiolength es 3
El segundo [...slots] empieza a leer desde el final, donde se quedó el primero. Solo un generador recién llamado empieza desde el principio.

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.

Arma la lista de números de socio de las solicitudes de membresía aprobadas. applications ya está declarado.

① Escribe una función generadora que elija solo las solicitudes aprobadas y devuelva sus números de socio de uno en uno.

② Obtén un generador, saca solo el primer número y muéstralo con la forma «Primero: M-4102».

③ Reúne el resto en un array y muéstralo con la forma «Quedan 2: M-4104, M-4105».

④ Muestra todos los números, incluido el primero, unidos con «, ».

Editor JavaScript / TypeScript

Ejecutar el código para ver el resultado

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)
La forma de sacar valores decide cuántas veces se llama a next()
serialFrom(1)numeración sin finDesestructurarconconst [a, b] =for...of muestrael 3 y hace break[...serialFrom(1)]para un arraynext() se llama2 vecesnext() se llama3 vecesdone nuncapasa a trueSolo generael 1 y el 2y terminaGenera del 1 al 3y sale del bucleLlama a next()sin parary nunca termina
Las dos columnas de la izquierda se detienen porque la cantidad de valores que se sacan es fija. El spread lee hasta que done es true, así que no puedes usarlo con una secuencia infinita.

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.

Calcula los tiempos de espera para reintentar una petición de red que falló, duplicándolos cada vez. retryDelays ya está declarado y entrega valores sin fin.

① Haz que take entregue solo los primeros count valores de source y luego termine.

② Empezando en 100, pon los 4 primeros valores en un array y muéstralos unidos con « → ».

③ Saca los 5 primeros valores con un bucle y muestra su suma con la forma «Total: 3100ms».

④ Pasa 0 como cantidad, convierte el resultado en un array y muestra su longitud.

Editor JavaScript / TypeScript

Ejecutar el código para ver el resultado
QUIZ

Verificación de conocimientos

Responde cada pregunta una a una.

Pregunta 1Justo después de llamar a una función declarada con function* y obtener un generador, ¿hasta dónde se ejecutó el cuerpo?

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?