Iteradores y generadores en JavaScript
En esta lección
Has recorrido con for...of arrays, cadenas, Set y Map, pero no un objeto normal. ¿Por qué unos sí y otros no? Porque los primeros cumplen el protocolo iterable: un pequeño acuerdo que dice “sé entregar mis valores de uno en uno”. En esta lección verás cómo funciona ese protocolo por dentro, cómo hacer que tus propios objetos se puedan recorrer y cómo los generadores (function* y yield) lo simplifican todo. Al final verás también los generadores infinitos y una pincelada de iteradores asíncronos.
Conviene que domines bucles, Set y Map y el operador spread.
Qué se puede recorrer con for...of
Un iterable es cualquier valor que se puede recorrer con for...of. En JavaScript ya lo son muchos tipos:
for (const letra of "sol") {
console.log(letra);
}
// s
// o
// l
const precios = new Map([["café", 1.2], ["té", 1.5]]);
for (const [producto, precio] of precios) {
console.log(`${producto}: ${precio} €`);
}
// café: 1.2 €
// té: 1.5 €
for (const n of new Set([3, 3, 7])) {
console.log(n);
}
// 3
// 7
| Iterable | Qué entrega en cada vuelta |
|---|---|
| Array | Cada elemento |
| Cadena | Cada carácter |
Set | Cada valor |
Map | Cada par [clave, valor] |
NodeList (de querySelectorAll) | Cada elemento del DOM |
arguments | Cada argumento |
Un objeto normal no es iterable:
const alumno = { nombre: "Eva", nota: 8 };
for (const x of alumno) {}
// TypeError: alumno is not iterable
Para recorrerlo tienes que pedir un iterable a partir de él, como Object.entries, que devuelve un array:
for (const [clave, valor] of Object.entries(alumno)) {
console.log(clave, valor);
}
// nombre Eva
// nota 8
El protocolo por dentro
Que algo sea iterable significa que tiene un método especial con un nombre raro: Symbol.iterator. Un símbolo es un tipo de valor que sirve como nombre de propiedad único, que no puede chocar con ningún otro; JavaScript usa Symbol.iterator como “la propiedad del iterador”.
Ese método devuelve un iterador: un objeto con un método next(). Cada vez que llamas a next(), te da un objeto con dos propiedades:
value: el valor siguiente.done:falsemientras quedan valores;truecuando se han acabado.
Puedes hacerlo a mano para ver lo que hace for...of por debajo:
const colores = ["rojo", "verde"];
const iterador = colores[Symbol.iterator]();
console.log(iterador.next()); // { value: 'rojo', done: false }
console.log(iterador.next()); // { value: 'verde', done: false }
console.log(iterador.next()); // { value: undefined, done: true }
for...of hace exactamente esto: pide el iterador, llama a next() una y otra vez, te pasa cada value y se detiene cuando done es true. Nunca necesitarás llamar a next() así en un bucle normal, pero entenderlo te permite crear tus propios iterables.
Nota: la diferencia entre los dos términos: un iterable es lo que se puede recorrer (un array); un iterador es el objeto que lleva la cuenta de por dónde vas en un recorrido concreto. Un mismo array puede dar muchos iteradores, uno por cada recorrido.
Crear un iterable propio (a mano)
Para que un objeto tuyo funcione con for...of, dale un método [Symbol.iterator] que devuelva un objeto con next(). Los corchetes son necesarios porque el nombre del método es un símbolo, no un texto:
const cuentaAtras = {
desde: 3,
[Symbol.iterator]() {
let actual = this.desde;
return {
next() {
if (actual > 0) {
return { value: actual--, done: false };
}
return { value: undefined, done: true };
},
};
},
};
for (const n of cuentaAtras) {
console.log(n);
}
// 3
// 2
// 1
console.log([...cuentaAtras]); // [ 3, 2, 1 ]
Funciona, y además, como es iterable, el spread ... también lo acepta. Pero hay mucho código de “fontanería” para algo tan simple. Para eso existen los generadores.
Generadores: function* y yield
Un generador es una función especial que puede pausarse y reanudarse. Se declara con un asterisco, function*, y dentro usa yield (“entregar”) para devolver un valor y quedarse en pausa en ese punto.
function* semaforo() {
yield "verde";
yield "ámbar";
yield "rojo";
}
Al llamar a un generador no se ejecuta su código. Lo que devuelve es un iterador. Cada next() ejecuta el código hasta el siguiente yield y se para ahí:
const luces = semaforo();
console.log(luces.next()); // { value: 'verde', done: false }
console.log(luces.next()); // { value: 'ámbar', done: false }
console.log(luces.next()); // { value: 'rojo', done: false }
console.log(luces.next()); // { value: undefined, done: true }
Y como lo que devuelve es iterable, lo normal es recorrerlo con for...of:
for (const color of semaforo()) {
console.log(color);
}
// verde
// ámbar
// rojo
La pausa, paso a paso
Para ver que de verdad se pausa, mete mensajes entre los yield:
function* conMensajes() {
console.log("empiezo");
yield 1;
console.log("sigo");
yield 2;
console.log("termino");
}
const g = conMensajes();
console.log("creado");
console.log(g.next().value);
console.log(g.next().value);
console.log(g.next().done);
// creado
// empiezo
// 1
// sigo
// 2
// termino
// true
“empiezo” no sale al crear el generador, sino con el primer next(). Y cada next() continúa justo donde se quedó, con todas sus variables locales intactas. Una función normal empieza siempre desde el principio; un generador recuerda por dónde iba.
El iterable propio, ahora con un generador
El método [Symbol.iterator] puede ser un generador. Basta con poner el asterisco delante del nombre:
const cuentaAtras2 = {
desde: 3,
*[Symbol.iterator]() {
for (let n = this.desde; n > 0; n--) {
yield n;
}
},
};
console.log([...cuentaAtras2]); // [ 3, 2, 1 ]
Mucho más corto que la versión a mano y hace lo mismo. En una clase es igual de sencillo. Con yield* puedes entregar uno a uno todos los valores de otro iterable:
class Playlist {
#canciones = [];
anadir(titulo) {
this.#canciones.push(titulo);
return this;
}
*[Symbol.iterator]() {
yield* this.#canciones; // entrega cada canción del array
}
}
const lista = new Playlist().anadir("Entre dos aguas").anadir("Malamente");
for (const cancion of lista) {
console.log(cancion);
}
// Entre dos aguas
// Malamente
Así el array interno sigue siendo privado, pero quien use la clase puede recorrerla como si fuera un array.
Generadores útiles
Un rango de números
JavaScript no tiene una función range como Python. Con un generador se escribe en tres líneas:
function* rango(inicio, fin, paso = 1) {
for (let i = inicio; i < fin; i += paso) {
yield i;
}
}
console.log([...rango(0, 10, 3)]); // [ 0, 3, 6, 9 ]
for (const piso of rango(1, 4)) {
console.log(`Revisando planta ${piso}`);
}
Generadores infinitos
Como un generador solo calcula el siguiente valor cuando se lo pides, puede tener un while (true) sin bloquear el programa:
function* idsPedido() {
let n = 1;
while (true) {
yield `PED-${String(n).padStart(4, "0")}`;
n++;
}
}
const ids = idsPedido();
console.log(ids.next().value); // PED-0001
console.log(ids.next().value); // PED-0002
Lo que no puedes hacer con un generador infinito es [...ids] o un for...of sin break: intentarían sacar todos los valores, que son infinitos, y la página se quedaría colgada.
Evaluación perezosa
Que los valores se calculen solo cuando hacen falta se llama evaluación perezosa (lazy). Permite encadenar generadores que procesan los datos de uno en uno, sin crear arrays intermedios:
function* naturales() {
let n = 1;
while (true) yield n++;
}
function* filtrar(iterable, condicion) {
for (const x of iterable) {
if (condicion(x)) yield x;
}
}
function* tomar(iterable, cuantos) {
if (cuantos <= 0) return;
for (const x of iterable) {
yield x;
if (--cuantos === 0) return;
}
}
const multiplosDe7 = filtrar(naturales(), (n) => n % 7 === 0);
console.log([...tomar(multiplosDe7, 4)]); // [ 7, 14, 21, 28 ]
Aunque naturales() es infinito, solo se generan los 28 primeros números, porque tomar deja de pedir en cuanto tiene 4. Con arrays sería imposible: tendrías que crear antes un array “suficientemente grande”.
Los navegadores modernos y Node 22 incluyen ya estos métodos en los propios iteradores (iterator helpers), así que puedes escribir lo mismo encadenando:
naturales().filter((n) => n % 2 === 0).map((n) => n * 10).take(3).toArray();
// [ 20, 40, 60 ]
Nota: si sales de un
for...ofconbreakoreturn, JavaScript cierra el generador: los siguientesnext()darándone: true. Si quieres seguir usándolo después, llama anext()a mano en vez de recorrerlo confor...of.
Todo lo que acepta un iterable
El protocolo no sirve solo para for...of. Todo esto funciona con cualquier iterable, sea un array, un Set o tu generador:
const [primero, segundo] = semaforo(); // desestructuración
console.log(primero, segundo); // verde ámbar
console.log(Array.from(rango(1, 4), (n) => n * n)); // [ 1, 4, 9 ]
console.log(Math.max(...rango(1, 6))); // 5
console.log(new Set(rango(0, 3)).size); // 3
| Construcción | Ejemplo |
|---|---|
for...of | for (const x of iterable) |
| Spread en arrays | [...iterable] |
| Spread en argumentos | Math.max(...iterable) |
| Desestructuración | const [a, b] = iterable |
Array.from | Array.from(iterable, fn) |
| Constructores | new Set(iterable), new Map(iterable) |
| Promesas | Promise.all(iterable) |
Fíjate en que el spread en objetos ({ ...obj }) es otra cosa: copia propiedades y no usa el protocolo iterable.
Un iterador solo se recorre una vez
Un array se puede recorrer cuantas veces quieras porque cada for...of le pide un iterador nuevo. Pero el objeto que devuelve un generador es el iterador, y cuando llega al final, se queda ahí:
const usado = semaforo();
console.log([...usado].length); // 3
console.log([...usado].length); // 0: ya estaba agotado
Si necesitas recorrerlo otra vez, llama de nuevo a la función generadora (semaforo()) o guarda los valores en un array.
Iteradores asíncronos: for await
A veces los valores llegan poco a poco y con espera: páginas de una API, lecturas de un sensor, líneas de un archivo grande. Para eso existen los generadores asíncronos (async function*), que pueden usar await dentro, y el bucle for await...of, que espera a cada valor:
const esperar = (ms) => new Promise((resolver) => setTimeout(resolver, ms));
async function* lecturas() {
for (let i = 1; i <= 3; i++) {
await esperar(100); // simula que el dato tarda en llegar
yield `Temperatura ${i}: ${18 + i} °C`;
}
}
for await (const lectura of lecturas()) {
console.log(lectura);
}
// Temperatura 1: 19 °C
// Temperatura 2: 20 °C
// Temperatura 3: 21 °C
for await solo se puede usar dentro de una función async o en el nivel superior de un módulo. Lo entenderás mejor después de las lecciones de asincronía y fetch; por ahora basta con que sepas que existe y que sirve para recorrer datos que llegan con el tiempo, como una API paginada.
Cuándo usar generadores
- Para que tus clases (una lista, un tablero, un inventario) se puedan recorrer con
for...ofsin exponer su array interno. - Para secuencias calculadas (rangos, IDs, fechas consecutivas) sin crear arrays enormes.
- Para secuencias infinitas o muy largas de las que solo necesitas una parte.
- Para recorrer datos que llegan por partes (con la versión asíncrona).
Si solo tienes unos pocos datos fijos, un array normal es más sencillo y se entiende mejor. No uses generadores por usarlos.
Errores frecuentes
- Usar
for...ofcon un objeto normal: dais not iterable. UsaObject.entries(obj),Object.keysoObject.values. - Olvidar el asterisco: con
functionsin*,yieldes un error de sintaxis. - Esperar que el generador se ejecute al llamarlo: llamarlo solo crea el iterador; el código corre con cada
next(). - Hacer spread de un generador infinito:
[...naturales()]no termina nunca. Limítalo antes con algo comotomarotake. - Recorrer dos veces el mismo iterador: la segunda vez está vacío. Llama de nuevo a la función generadora.
- Escribir
Symbol.iteratorsin corchetes:{ Symbol.iterator() {...} }no es válido; el nombre es un símbolo, así que va entre corchetes:[Symbol.iterator](). - Confundir
for...ofconfor...in:for...inrecorre los nombres de las propiedades de un objeto, no los valores de un iterable.
Resumen
| Concepto | Qué es |
|---|---|
| Iterable | Algo que se puede recorrer con for...of: tiene [Symbol.iterator]() |
| Iterador | Objeto con next() que devuelve { value, done } |
function* | Declara un generador |
yield valor | Entrega un valor y pausa |
yield* iterable | Entrega todos los valores de otro iterable |
*[Symbol.iterator]() | Forma corta de hacer iterable un objeto o una clase |
| Evaluación perezosa | Calcular cada valor solo cuando se pide |
async function* + for await | Recorrer valores que llegan con espera |
- Arrays, cadenas,
Set,MapyNodeListson iterables; los objetos normales, no. - Spread, desestructuración,
Array.fromynew Setaceptan cualquier iterable.
En la siguiente lección aprenderás a detectar y gestionar los fallos de tu programa: errores y excepciones.
Pruébalo tú
Cambia el código y pulsa Ejecutar (o Ctrl + Enter).
Pon a prueba lo que has aprendido
¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.