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:
// 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ó—.
// 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
Lo importante: for...of no hace magia, hace literalmente esto por ti —pedir el iterador y llamar
a next() hasta que done es true—:
// 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.
// 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:
// 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:
// 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:
// 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:
// 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:
// 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.
Paso 2: Que esté pulido
- Produces los turnos con un generador (function* + yield).
- Recorres el generador con for...of, sin precalcular el array.
- El estado del recorrido vive dentro del generador, no en variables sueltas.
Paso 3: Que sea excelente
- Separas responsabilidades en generadores reutilizables (ids por un lado, orden de equipos por otro).
- Los compones con destructuring y materializas solo lo que muestras (un tomar(iterable, n)).
- Dejas claro, en comentarios, por qué lo perezoso gana aquí y cuándo sobraría.
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.
// ════════════════════════════════════════════════════════════════════════════
// NIVEL MEJOR — "pulido"
//
// Por qué mejora a OK:
// - turnosDeDraft es un GENERADOR (function*): no precalcula un array. Cada
// yield emite un turno y pausa ahí; el for...of pide el siguiente cuando lo
// necesita. Producción BAJO DEMANDA en vez de toda de golpe.
// - desaparece el contador suelto: el índice del bucle (i) vive dentro del
// generador, encapsulado, sin variables de estado por fuera.
//
// Su límite respecto a Excelente: el generador hace DOS cosas a la vez (alternar
// equipos y fabricar ids), atadas a este draft concreto; ninguna de las dos se
// puede reutilizar suelta. El id se calcula inline con el índice.
// ════════════════════════════════════════════════════════════════════════════
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" },
];
// Generador: produce los turnos del draft uno a uno. function* lo declara;
// cada yield emite un turno y deja la función pausada ahí hasta el siguiente next().
function* turnosDeDraft(equipos, pool) {
for (let i = 0; i < pool.length; i++) {
yield {
// el número de turno
numero: i + 1,
// alterna Azul, Rojo, Azul…
equipo: equipos[i % equipos.length],
// el héroe que toca elegir
heroe: pool[i].nombre,
// su rol
rol: pool[i].rol,
// su id de pick (P-001…)
pickId: "P-" + String(i + 1).padStart(3, "0"),
};
}
}
// Muestra un turno en la consola.
function mostrarTurno(turno) {
console.log(
"Turno " +
turno.numero +
" | " +
turno.equipo +
" | " +
turno.heroe +
" (" +
turno.rol +
") | " +
turno.pickId,
);
}
// Recorremos el generador con for...of: cada vuelta consume un yield (un turno).
console.log("--- Orden del draft ---");
for (const turno of turnosDeDraft(EQUIPOS, POOL)) {
// muestra el turno de este yield
mostrarTurno(turno);
} Por qué es mejor que el anterior
- turnosDeDraft es un generador (function*): emite cada turno con yield y el for...of pide el siguiente cuando lo necesita. Producción bajo demanda, sin array precalculado.
- El estado (el índice del turno) queda encapsulado dentro del generador, sin variables sueltas por fuera.
- Todavía mezcla dos responsabilidades en un solo sitio —alternar equipos y fabricar ids—, atadas a este draft: eso lo separa el nivel Excelente.
// ════════════════════════════════════════════════════════════════════════════
// NIVEL EXCELENTE — "óptimo"
//
// Por qué mejora a Mejor:
// - Cada responsabilidad es su propio generador REUTILIZABLE: secuenciaDeIds
// (los ids de pick) y ordenDeEquipos (a quién le toca). Ambos INFINITOS: no
// saben cuántos turnos habrá; producen sin fin, uno por next().
// - draft() los COMPONE con destructuring ({ value: equipo } = turnos.next()):
// un generador finito construido sobre dos infinitos.
// - tomar() aplica el protocolo de iteración a mano (Symbol.iterator + next) y
// solo materializa los valores que pides. Esa es la ventaja real de lo
// perezoso: la secuencia podría ser infinita y nunca la construyes entera.
//
// Cuándo esto sobra: para 8 turnos, el bucle del nivel OK basta. Los generadores
// brillan en secuencias grandes, infinitas o caras de calcular (paginación,
// streams, ids sin fin). No metas generadores por deporte; mete cada herramienta
// donde su ventaja se note.
// ════════════════════════════════════════════════════════════════════════════
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" },
];
// Surtidor INFINITO de ids: P-001, P-002, P-003… uno por cada next(), sin fin.
// while (true) no cuelga el programa: solo produce cuando alguien pide un valor.
function* secuenciaDeIds(prefijo) {
// contador interno, vive entre pausas
let n = 1;
while (true) {
// emite el id y se pausa
yield prefijo + "-" + String(n).padStart(3, "0");
// al volver (siguiente next), sigue justo aquí
n++;
}
}
// Surtidor INFINITO del orden de equipos: Azul, Rojo, Azul, Rojo… sin fin.
function* ordenDeEquipos(equipos) {
let i = 0;
while (true) {
// alterna dando la vuelta con el módulo
yield equipos[i % equipos.length];
i++;
}
}
// Toma los primeros n valores de CUALQUIER iterable, incluso uno infinito.
// Aquí está la clave: solo materializa lo que pides; el resto nunca se calcula.
function tomar(iterable, n) {
// su iterador (el protocolo, a mano)
const it = iterable[Symbol.iterator]();
const out = [];
for (let i = 0; i < n; i++) {
// pide el siguiente valor
const { value, done } = it.next();
// si el iterable se agotó antes de tiempo, paramos
if (done) break;
out.push(value);
}
return out;
}
// El draft real: empareja cada héroe del pool con el siguiente equipo y el
// siguiente id. Generador FINITO (recorre el pool) montado sobre dos infinitos.
function* draft(equipos, pool) {
// su propio surtidor de ids
const ids = secuenciaDeIds("P");
// su propio surtidor de equipos
const turnos = ordenDeEquipos(equipos);
let numero = 1;
for (const heroe of pool) {
// next() devuelve { value, done }; con destructuring extraemos 'value' y,
// opcionalmente, le damos un alias para que el nombre describa qué contiene.
const { value: equipo } = turnos.next();
// mismo patrón: extraemos 'value' del objeto { value, done } con alias 'pickId'
const { value: pickId } = ids.next();
yield { numero, equipo, heroe: heroe.nombre, rol: heroe.rol, pickId };
numero++;
}
}
// Muestra un turno en la consola.
function mostrarTurno(turno) {
console.log(
"Turno " +
turno.numero +
" | " +
turno.equipo +
" | " +
turno.heroe +
" (" +
turno.rol +
") | " +
turno.pickId,
);
}
// tomar() pide solo POOL.length turnos: la secuencia perezosa nunca se construye
// entera. Cambia POOL por uno de mil héroes y el coste por turno es el mismo.
const turnos = tomar(draft(EQUIPOS, POOL), POOL.length);
// muestra todos los turnos por la consola
console.log("--- Orden del draft ---");
for (const turno of turnos) {
mostrarTurno(turno);
} Por qué es mejor que el anterior
- Cada responsabilidad es su propio generador infinito y reutilizable (secuenciaDeIds, ordenDeEquipos); draft() los compone con destructuring sobre next().
- tomar() aplica el protocolo de iteración a mano (Symbol.iterator + next) y solo materializa los turnos que pides: la secuencia podría ser infinita y nunca se construye entera.
- Comenta cuándo esto sobra: para 8 turnos basta un bucle. Los generadores brillan en lo grande, infinito o caro; no se meten por deporte.