Saltar al contenido
iteradores-y-generadores.js · devschool

Iteradores y generadores en JavaScript

Lección 20 de 43 · 12 min de lectura · Actualizado el

En esta lección
  1. Qué se puede recorrer con for...of
  2. El protocolo por dentro
  3. Crear un iterable propio (a mano)
  4. Generadores: function* y yield
  5. Generadores útiles
  6. Todo lo que acepta un iterable
  7. Un iterador solo se recorre una vez
  8. Iteradores asíncronos: for await
  9. Cuándo usar generadores
  10. Errores frecuentes
  11. Resumen

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
IterableQué entrega en cada vuelta
ArrayCada elemento
CadenaCada carácter
SetCada valor
MapCada par [clave, valor]
NodeList (de querySelectorAll)Cada elemento del DOM
argumentsCada 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: false mientras quedan valores; true cuando 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...of con break o return, JavaScript cierra el generador: los siguientes next() darán done: true. Si quieres seguir usándolo después, llama a next() a mano en vez de recorrerlo con for...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ónEjemplo
for...offor (const x of iterable)
Spread en arrays[...iterable]
Spread en argumentosMath.max(...iterable)
Desestructuraciónconst [a, b] = iterable
Array.fromArray.from(iterable, fn)
Constructoresnew Set(iterable), new Map(iterable)
PromesasPromise.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...of sin 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...of con un objeto normal: da is not iterable. Usa Object.entries(obj), Object.keys o Object.values.
  • Olvidar el asterisco: con function sin *, yield es 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 como tomar o take.
  • Recorrer dos veces el mismo iterador: la segunda vez está vacío. Llama de nuevo a la función generadora.
  • Escribir Symbol.iterator sin corchetes: { Symbol.iterator() {...} } no es válido; el nombre es un símbolo, así que va entre corchetes: [Symbol.iterator]().
  • Confundir for...of con for...in: for...in recorre los nombres de las propiedades de un objeto, no los valores de un iterable.

Resumen

ConceptoQué es
IterableAlgo que se puede recorrer con for...of: tiene [Symbol.iterator]()
IteradorObjeto con next() que devuelve { value, done }
function*Declara un generador
yield valorEntrega un valor y pausa
yield* iterableEntrega todos los valores de otro iterable
*[Symbol.iterator]()Forma corta de hacer iterable un objeto o una clase
Evaluación perezosaCalcular cada valor solo cuando se pide
async function* + for awaitRecorrer valores que llegan con espera
  • Arrays, cadenas, Set, Map y NodeList son iterables; los objetos normales, no.
  • Spread, desestructuración, Array.from y new Set aceptan 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).

iteradores-y-generadores.js
Consola

Pon a prueba lo que has aprendido

[JavaScript] ¿Qué imprime este programa, en este orden?
function* gen() {
  console.log("a");
  yield 1;
  console.log("b");
  yield 2;
}
const it = gen();
console.log("c");
console.log(it.next().value);

[JavaScript] ¿Qué imprime?
function* r() { yield 1; yield 2; yield 3; }
const u = r();
console.log([...u].length, [...u].length);

[JavaScript] ¿Qué ocurre con for (const x of { a: 1, b: 2 }) {}?

[JavaScript] Tienes un generador infinito naturales(). ¿Cuál de estas líneas cuelga el programa?

¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.