Iteradores y generadores

Avanzado
JavaScript
JavaScript
Actualizado: 19/04/2026

Qué es el protocolo de iteración

Cuando en JavaScript escribes for (const x of lista) o [...iterable], el motor no asume que estás trabajando con un array. Lo que hace es invocar el protocolo de iteración: un contrato formado por dos piezas.

  • Un objeto es iterable si tiene una propiedad con la clave Symbol.iterator, cuyo valor es una función que devuelve un iterador.
  • Un objeto es un iterador si tiene un método next() que devuelve un objeto { value, done }. Cuando done vale true, la iteración ha terminado.
const numbers = [10, 20, 30];
const iterator = numbers[Symbol.iterator]();

console.log(iterator.next()); // { value: 10, done: false }
console.log(iterator.next()); // { value: 20, done: false }
console.log(iterator.next()); // { value: 30, done: false }
console.log(iterator.next()); // { value: undefined, done: true }

Los arrays, strings, Map, Set y arguments ya implementan este protocolo. Cualquier estructura que quiera participar en for...of, spread o desestructuración debe exponer Symbol.iterator.

Crear un iterable personalizado

Veamos cómo convertir una clase propia en algo iterable. El ejemplo clásico es un rango numérico:

class Range {
  constructor(start, end, step = 1) {
    this.start = start;
    this.end = end;
    this.step = step;
  }

  [Symbol.iterator]() {
    let current = this.start;
    const { end, step } = this;

    return {
      next() {
        if (current <= end) {
          const value = current;
          current += step;
          return { value, done: false };
        }
        return { value: undefined, done: true };
      }
    };
  }
}

const range = new Range(1, 10, 2);

for (const n of range) {
  console.log(n); // 1, 3, 5, 7, 9
}

console.log([...range]); // [1, 3, 5, 7, 9]
console.log(Array.from(range, n => n ** 2)); // [1, 9, 25, 49, 81]

Lo importante es que el iterador es independiente del iterable: cada llamada a Symbol.iterator produce uno nuevo. Por eso podemos recorrer el rango dos veces sin agotarlo.

Iterador que además es iterable

Un patrón cómodo es que el propio iterador sea iterable (se devuelva a sí mismo en Symbol.iterator). Así puedes reanudar una iteración a medias desde un for...of:

function counter() {
  let i = 0;
  return {
    next() { return { value: i++, done: false }; },
    [Symbol.iterator]() { return this; }
  };
}

const c = counter();
c.next(); // { value: 0 }
c.next(); // { value: 1 }

for (const n of c) {
  if (n > 4) break;
  console.log(n); // 2, 3, 4
}

Funciones generadoras

Escribir iteradores a mano es verboso. Las funciones generadoras (function*) hacen todo el trabajo: basta con usar yield y el motor construye por ti un iterador compatible con el protocolo.

function* range(start, end, step = 1) {
  for (let i = start; i <= end; i += step) {
    yield i;
  }
}

for (const n of range(1, 10, 2)) {
  console.log(n); // 1, 3, 5, 7, 9
}

Lo que sucede por debajo es lo mismo que el ejemplo manual: llamar a range(1, 10, 2) no ejecuta el cuerpo, sino que devuelve un iterador. Cada next() reanuda la función hasta el siguiente yield, y devuelve { value, done: false }. Cuando la función retorna, el resultado pasa a { value: undefined, done: true }.

Estado preservado entre yields

El generador guarda todo su estado local entre llamadas, lo que permite lógicas que serían incómodas con iteradores manuales:

function* pairs(array) {
  for (let i = 0; i < array.length; i += 2) {
    yield [array[i], array[i + 1]];
  }
}

console.log([...pairs(["a", 1, "b", 2, "c", 3])]);
// [ ["a", 1], ["b", 2], ["c", 3] ]

Secuencias infinitas y pereza

Al ejecutarse paso a paso, un generador puede modelar una secuencia infinita sin colapsar la memoria. El consumidor decide cuántos valores toma:

function* naturals() {
  let i = 1;
  while (true) {
    yield i++;
  }
}

function take(iterable, count) {
  const result = [];
  for (const value of iterable) {
    if (result.length >= count) break;
    result.push(value);
  }
  return result;
}

console.log(take(naturals(), 5)); // [1, 2, 3, 4, 5]

Esta naturaleza perezosa es especialmente útil para representar streams de datos, cursores paginados o cálculos caros que no siempre se consumen completos.

Delegación con yield*

yield* delega la producción de valores a otro iterable: todos sus valores pasan a formar parte de la secuencia del generador actual. Es la manera idiomática de componer generadores.

function* letters() {
  yield "a";
  yield "b";
  yield "c";
}

function* digits() {
  yield 1;
  yield 2;
  yield 3;
}

function* mix() {
  yield* letters();
  yield "-";
  yield* digits();
}

console.log([...mix()]); // ["a", "b", "c", "-", 1, 2, 3]

Como los arrays ya son iterables, también se pueden delegar directamente:

function* flatten(array) {
  for (const item of array) {
    if (Array.isArray(item)) {
      yield* flatten(item);
    } else {
      yield item;
    }
  }
}

console.log([...flatten([1, [2, [3, [4, 5]]], 6])]); // [1, 2, 3, 4, 5, 6]

El aplanado recursivo queda trivial y, al ser un generador, es perezoso: consume el árbol solo a medida que lo recorres.

Pipelines perezosos

Combinando varios generadores se obtienen pipelines de transformación equivalentes a map y filter, pero sin crear arrays intermedios:

function* map(iterable, fn) {
  for (const value of iterable) yield fn(value);
}

function* filter(iterable, predicate) {
  for (const value of iterable) {
    if (predicate(value)) yield value;
  }
}

function* take(iterable, count) {
  let i = 0;
  for (const value of iterable) {
    if (i++ >= count) return;
    yield value;
  }
}

// Primeros 5 cuadrados pares de los naturales
const result = take(
  filter(
    map(naturals(), n => n * n),
    n => n % 2 === 0
  ),
  5
);

console.log([...result]); // [4, 16, 36, 64, 100]

Solo se calcula lo estrictamente necesario para producir los 5 valores pedidos, aunque la fuente (naturals) sea infinita.

Comunicación bidireccional: next(value) y return

El método next acepta un argumento que se convierte en el valor de la expresión yield dentro del generador. Esto permite que el consumidor inyecte datos en la función:

function* dialog() {
  const name = yield "¿Cómo te llamas?";
  const age  = yield `Hola, ${name}. ¿Cuántos años tienes?`;
  return `${name} tiene ${age} años.`;
}

const it = dialog();
console.log(it.next().value);        // "¿Cómo te llamas?"
console.log(it.next("Alan").value);  // "Hola, Alan. ¿Cuántos años tienes?"
console.log(it.next(34).value);      // "Alan tiene 34 años."

Además de next, un iterador puede exponer return(value) para terminar de forma anticipada y throw(error) para inyectar una excepción dentro del generador. for...of invoca return cuando sale del bucle con break, lo que permite a los generadores ejecutar código de limpieza con try/finally.

Buenas prácticas

  • Usa for...of o spread siempre que puedas; evita llamar a next() a mano salvo que necesites una iteración manual muy concreta.
  • Prefiere generadores a iteradores escritos a mano: son más cortos, más claros y gestionan solos los estados terminales.
  • Aprovecha yield* para componer generadores en lugar de copiar lógica; permite encadenar transformaciones sin coste de memoria.
  • Para secuencias potencialmente infinitas, define un combinador como take que limite la cantidad tomada; nunca hagas [...infinite].
  • Si el generador mantiene recursos externos (un cursor de base de datos, un archivo), libéralos en un bloque finally para que se ejecuten también en break o throw.

Resumen

Iteradores y generadores son las piezas que hacen posibles construcciones como for...of, spread y la desestructuración. Implementar Symbol.iterator convierte cualquier objeto en iterable, y function* + yield reduce ese trabajo a unas pocas líneas, con soporte natural para secuencias perezosas, infinitas y composables. Una vez interiorizado el protocolo, se abre la puerta a diseñar APIs que procesan datos streaming sin cargar todo en memoria.

Alan Sastre - Autor del tutorial

Alan Sastre

Ingeniero de Software y formador, CEO en CertiDevs

Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, JavaScript es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.

Más tutoriales de JavaScript

Explora más contenido relacionado con JavaScript y continúa aprendiendo con nuestros tutoriales gratuitos.

Aprendizajes de esta lección

  • Entender el protocolo iterable/iterator y cómo lo usa for...of.
  • Implementar un iterable personalizado con Symbol.iterator.
  • Crear funciones generadoras con function* y controlar el flujo con yield.
  • Producir secuencias perezosas e incluso infinitas sin colapsar la memoria.
  • Combinar generadores con yield* para delegar en otros iterables.
  • Construir pipelines de transformación basados en iteradores.

Cursos que incluyen esta lección

Esta lección forma parte de los siguientes cursos estructurados con rutas de aprendizaje