learning-front

Extra · JavaScript a fondo (opcional)

Iteradores y generadores

Qué hace que algo sea recorrible con for...of y spread —el protocolo de iteración— y cómo fabricar secuencias perezosas, incluso infinitas, con generadores.

Ya recorres listas con for...of, map, filter y compañía sin pensar en cómo lo hace el motor por debajo. Este capítulo abre esa caja: qué convierte a un valor en algo recorrible (el protocolo de iteración) y cómo fabricar tus propias secuencias —incluso infinitas— que se producen poco a poco, con generadores. No es sintaxis del día a día, pero es el mecanismo sobre el que se apoyan for...of, el spread y media librería estándar.

Seguimos con el Team Builder. El hilo de este capítulo es un draft de torneo: dos equipos eligen héroes por turnos. Generar ese orden turno a turno, sin precalcularlo entero, es el ejemplo perfecto de secuencia perezosa.

Todo lo que recorres con for...of es un iterable#

En el nivel 2 usaste for...of con arrays, strings, Set y Map. Cuatro tipos muy distintos, y los cuatro se recorren exactamente igual:

javascript
// Los cuatro responden al mismo for...of, sin que cambies nada de la sintaxis:
// un array -> Tracer, Genji
for (const heroe of ["Tracer", "Genji"]) console.log(heroe);
// un string -> O, W
for (const letra of "OW") console.log(letra);
// un Set -> Tanque, Daño
for (const rol of new Set(["Tanque", "Daño"])) console.log(rol);
// un Map -> a=1
for (const [k, v] of new Map([["a", 1]])) console.log(k + "=" + v);

No es casualidad ni un caso especial del lenguaje para cada tipo. Los cuatro cumplen el mismo contrato: son iterables. Y cualquier cosa que cumpla ese contrato —incluida una tuya— se podrá recorrer con for...of y desplegar con spread.

El protocolo por dentro: una clave especial y next()#

El contrato es este: un iterable tiene un método guardado bajo una clave especial incorporada, Symbol.iterator. Ese método devuelve un iterador: un objeto con un método next(). Cada llamada a next() devuelve un objeto { value, done } —el siguiente valor y si la secuencia ya se acabó—.

javascript
// un Set, que es iterable
const roles = new Set(["Tanque", "Daño", "Apoyo"]);
// pedimos su ITERADOR con la clave especial
const it = roles[Symbol.iterator]();

// { value: 'Tanque', done: false }  -> primer valor
console.log(it.next());
// { value: 'Daño',   done: false }
console.log(it.next());
// { value: 'Apoyo',  done: false }
console.log(it.next());
// { value: undefined, done: true }  -> se acabó: done es true
console.log(it.next());

Symbol.iterator es una clave única que el lenguaje reserva justo para esto. Los símbolos los verás a fondo en el próximo capítulo; aquí basta con saber que es una clave incorporada que conecta tu objeto con el protocolo.

Lo importante: for...of no hace magia, hace literalmente esto por ti —pedir el iterador y llamar a next() hasta que done es true—:

javascript
// Esto, escrito a mano…
// cogemos el iterador del Set
const it = roles[Symbol.iterator]();
// primer { value, done }
let paso = it.next();
while (!paso.done) {
  // mientras no se haya acabado…
  // usamos el valor de este paso
  console.log(paso.value);
  // …y pedimos el siguiente
  paso = it.next();
}

// …es EXACTAMENTE lo que hace esto por debajo:
// mismo resultado, sin el andamiaje
for (const rol of roles) console.log(rol);

El spread (...) usa el mismo protocolo: consume el iterable de principio a fin y vuelca los valores donde lo pongas.

javascript
// spread recorre el iterable hasta done y lo vuelca en un array
const comoArray = [...roles];
// comoArray -> ['Tanque', 'Daño', 'Apoyo']

Generadores: secuencias que se producen bajo demanda#

Implementar el protocolo a mano (un objeto con Symbol.iterator y next()) es engorroso. Por eso existe una forma cómoda de fabricar iteradores: los generadores. Se declaran con function* (con asterisco) y usan yield para emitir valores.

La diferencia con una función normal es radical: llamar a un generador no ejecuta su cuerpo. Devuelve un iterador en pausa. El código de dentro corre a trocitos, cada vez que pides un valor:

javascript
// function* declara un generador. Cada yield emite un valor y pausa la función ahí.
function* primerosIds() {
  // emite 'P-001' y se queda pausado en esta línea
  yield "P-001";
  // en el siguiente next(), reanuda y emite 'P-002'
  yield "P-002";
  // y en el siguiente, 'P-003'
  yield "P-003";
}

// NO ejecuta nada: devuelve un iterador en pausa
const ids = primerosIds();
// { value: 'P-001', done: false }  -> corre hasta el 1er yield
console.log(ids.next());
// { value: 'P-002', done: false }  -> reanuda hasta el 2º
console.log(ids.next());
// 'P-003'                     -> solo el valor del 3er yield
console.log(ids.next().value);
// { value: undefined, done: true }  -> se acabó el cuerpo
console.log(ids.next());

Lo potente es que un generador conserva su estado entre pausas. Eso permite secuencias infinitas: producen sin fin, pero solo un valor cada vez que se lo pides. El bucle while (true) no cuelga el programa, porque el generador se duerme en cada yield:

javascript
// Surtidor infinito de ids de pick: P-001, P-002, P-003… sin fin.
function* secuenciaDeIds(prefijo) {
  // este estado sobrevive a cada pausa
  let n = 1;
  while (true) {
    // "infinito", pero perezoso: no calcula nada hasta que le pides un valor
    // emite el id y se pausa
    yield prefijo + "-" + String(n).padStart(3, "0");
    // al volver (siguiente next), continúa justo aquí
    n++;
  }
}

// un iterador infinito, sin coste todavía
const ids = secuenciaDeIds("P");
// 'P-001'
console.log(ids.next().value);
// 'P-002'
console.log(ids.next().value);
// 'P-003'  -> pides tres, produce tres; el resto no existe aún
console.log(ids.next().value);

padStart(3, "0") rellena el número con ceros a la izquierda hasta tres cifras (1"001"): un método de string del nivel 2, aquí al servicio de un id con pinta de id.

Un generador es iterable: for...of y spread#

Un generador devuelve un iterador que, además, es iterable de sí mismo. Traducido: puedes recorrerlo con for...of y desplegarlo con spread, igual que un array. Eso sí, solo si es finito:

javascript
// Generador FINITO: para cuando llega a 'cuantos' turnos.
function* turnos(equipos, cuantos) {
  for (let i = 0; i < cuantos; i++) {
    // alterna con el módulo y emite el equipo del turno
    yield equipos[i % equipos.length];
  }
}

// for...of lo recorre como a cualquier iterable:
for (const equipo of turnos(["Azul", "Rojo"], 4)) {
  // Azul, Rojo, Azul, Rojo
  console.log("pica: " + equipo);
}

// spread lo consume entero y lo vuelca en un array:
const orden = [...turnos(["Azul", "Rojo"], 4)];
// orden -> ['Azul', 'Rojo', 'Azul', 'Rojo']

Aviso que ahorra un cuelgue: nunca hagas for...of ni spread sobre un generador infinito sin un límite. Como nunca emite done: true, el spread se quedaría pidiendo valores para siempre y bloquearía la página. Para consumir un trozo de una secuencia infinita, pide con next() solo las veces que necesites:

javascript
// infinito: NO lo despliegues con [...fuente]
const fuente = secuenciaDeIds("P");
// Pedimos exactamente tres ids y paramos:
const tres = [fuente.next().value, fuente.next().value, fuente.next().value];
// tres -> ['P-001', 'P-002', 'P-003']

Esto, que parece un truco de salón, es justo lo que hay debajo de cosas muy reales: una paginación que pide la siguiente página solo cuando haces scroll, un stream que entrega datos según llegan, o una fuente de ids que nunca se agota. En todos, la regla es la misma: no calcules la secuencia entera; descríbela y deja que quien la consume decida cuánto producir.

El caso real: recorrer una API paginada#

Dejemos el draft un momento, porque este es el ejemplo que de verdad te vas a encontrar en una empresa. Un backend no te devuelve los 10.000 resultados de una búsqueda de golpe: te los sirve por páginas. Pides la página 1 (20 resultados), luego la 2, y así. El código que pinta la lista no debería tener que saber de páginas, índices ni “¿queda alguna más?”: eso es fontanería.

Un generador esconde esa fontanería. Envuelve el ir pidiendo páginas y expone una secuencia plana de elementos; quien la consume la recorre con for...of como si fuera un array normal, y la página siguiente se pide sola, solo cuando hace falta:

javascript
// Simulamos el servidor: una lista grande que solo se entrega a trozos.
const SERVIDOR = ["Ana", "Bruno", "Carla", "Diego", "Eva", "Fran", "Gema"];

// pedirPagina devuelve UN trozo: del índice 'desde' hasta 'desde + tamano'.
// En una app real, aquí dentro iría un fetch() al backend y un await.
function pedirPagina(desde, tamano) {
  // devuelve [] cuando ya no quedan datos
  return SERVIDOR.slice(desde, desde + tamano);
}

// El generador produce los elementos página a página. Pide la SIGUIENTE página
// solo cuando ha agotado la actual, y para en cuanto una página vuelve vacía.
function* recorrerTodo(tamano) {
  // por qué posición de la lista vamos
  let desde = 0;
  while (true) {
    // pide la siguiente página
    const pagina = pedirPagina(desde, tamano);
    // página vacía: no hay más, fin de la secuencia
    if (pagina.length === 0) return;
    // emite los elementos de esta página, uno a uno
    for (const item of pagina) yield item;
    // avanza al inicio de la siguiente página
    desde += tamano;
  }
}

// El consumidor NO sabe nada de páginas: recorre como una lista cualquiera.
for (const nombre of recorrerTodo(3)) {
  // Ana, Bruno, Carla, Diego… pidiendo páginas según avanza
  console.log(nombre);
}

Fíjate en el reparto: recorrerTodo sabe cómo se obtienen los datos (en páginas de cierto tamaño); el for...of solo sabe qué quiere (los elementos, en orden). Esa frontera es la que hace al código de la lista indiferente a si los datos vienen de un array, de tres páginas o de tres mil. El mismo patrón está detrás del scroll infinito (pide la página siguiente al llegar al final) y de leer un stream de datos según llegan: describe la secuencia y deja que quien la consume marque el ritmo.

Pruébalo tú#

Mira la consola: el protocolo de un Set a mano, un generador de ids infinito del que pedimos solo tres, un generador finito que recorremos con for...of y desplegamos con spread, y el caso real — recorrer una API paginada sin pedirla entera—. Cambia el 4 de turnos por otro número, pide más ids con ids.next().value, o ajusta el tamaño de página en recorrerTodo(3) y observa cómo cambia el reparto en trozos sin tocar el bucle que consume. Pulsa Ejecutar (o Ctrl+Enter) para ver la consola.

Comprueba lo que sabes#

Pregunta 1 de 5

¿Qué tienen en común un array, un string, un Set y un Map para poder recorrerse los cuatro con for...of?

Tu turno#

Genera el orden de un draft: dos equipos eligen héroes por turnos alternos de un pool. Empieza con un bucle si lo necesitas, pero el objetivo es producirlo con un generador, bajo demanda. Cuando lo tengas (o si te atascas), despliega las soluciones y fíjate en cómo el nivel Excelente parte el problema en generadores reutilizables y solo materializa lo que pinta.

Ejercicio · en esta página

Generador de turnos de pick

Dos equipos (Azul y Rojo) eligen héroes por turnos alternos de un pool compartido. Produce el orden del draft turno a turno —número, equipo, héroe, rol e id de pick (P-001, P-002…)— y muéstralo por la consola. La clave: prodúcelo bajo demanda con un generador, sin precalcular un array gigante.

Paso 1: Que funcione

  • Sale una línea por turno en la consola, alternando Azul y Rojo.
  • Cada turno muestra héroe, rol e id de pick correlativo (P-001, P-002…).
  • Vale resolverlo con un bucle for que construya el array de turnos.
Ver soluciones
// ════════════════════════════════════════════════════════════════════════════
// NIVEL OK — "funciona"
//
// Precalcula TODOS los turnos de una vez con un bucle for y un array que se va
// llenando. El draft sale correcto, pero...
//   - materializa la secuencia ENTERA de golpe: con 8 turnos da igual, pero esta
//     forma no escala a secuencias grandes, caras de calcular o infinitas.
//   - el id de pick es una variable suelta (contadorId) que arrastras a mano por
//     el bucle: estado mezclado con la lógica del turno.
//   - no hay ninguna pieza reutilizable: si mañana quieres los ids o el orden de
//     equipos en otro sitio, te toca copiar el bucle.
// Funciona, pero es justo el caso que un generador resuelve más limpio.
// ════════════════════════════════════════════════════════════════════════════

const EQUIPOS = ["Azul", "Rojo"];
const POOL = [
  { nombre: "Tracer", rol: "Daño" },
  { nombre: "Reinhardt", rol: "Tanque" },
  { nombre: "Mercy", rol: "Apoyo" },
  { nombre: "Genji", rol: "Daño" },
  { nombre: "Ana", rol: "Apoyo" },
  { nombre: "Winston", rol: "Tanque" },
  { nombre: "Sojourn", rol: "Daño" },
  { nombre: "Brigitte", rol: "Apoyo" },
];

// Muestra un turno en la consola.
function mostrarTurno(turno) {
  console.log(
    "Turno " +
      turno.numero +
      " | " +
      turno.equipo +
      " | " +
      turno.heroe +
      " (" +
      turno.rol +
      ") | " +
      turno.pickId,
  );
}

// Construimos el array COMPLETO de turnos con un bucle for.
// se llena de golpe; nada se produce bajo demanda
const turnos = [];
// el id de pick: variable suelta que subimos a mano
let contadorId = 1;
for (let i = 0; i < POOL.length; i++) {
  // alterna Azul, Rojo, Azul…
  const equipo = EQUIPOS[i % EQUIPOS.length];
  // P-001, P-002…
  const pickId = "P-" + String(contadorId).padStart(3, "0");
  // siguiente id para el próximo turno
  contadorId++;
  turnos.push({
    // el número de turno
    numero: i + 1,
    // el equipo que pica
    equipo,
    // el héroe que toca
    heroe: POOL[i].nombre,
    // su rol
    rol: POOL[i].rol,
    // su id de pick
    pickId,
  });
}

// muestra todos los turnos por la consola
console.log("--- Orden del draft ---");
for (const turno of turnos) {
  mostrarTurno(turno);
}

Por qué este nivel

  • Precalcula TODOS los turnos de golpe con un bucle for y arrastra un contador de ids suelto. Funciona y es lo más directo de leer.
  • Su límite: materializa la secuencia entera (con 8 da igual, pero no escala a secuencias grandes o infinitas) y no deja ninguna pieza reutilizable.