Módulo 11: Colecciones, iteradores y generadores

Objetivos del módulo

  • Usar Map para diccionarios con claves de cualquier tipo y saber cuándo preferirlo a un objeto
  • Aprovechar getOrInsert y getOrInsertComputed (ES2026) para contar, agrupar y cachear
  • Usar Set para valores únicos y las operaciones de conjuntos de ES2025 (union, intersection…)
  • Entender para qué sirven WeakMap, WeakSet y WeakRef
  • Conocer los Symbol: únicos, globales y los símbolos conocidos del lenguaje
  • Comprender el protocolo de iteración y crear tus propios objetos iterables
  • Escribir generadores con function*, yield y yield*, incluidos generadores infinitos
  • Encadenar iterator helpers (ES2025) con evaluación perezosa, y unir iteradores con Iterator.concat (ES2026)
  • Tener una primera idea de los generadores asíncronos y for await...of

1. Map: diccionarios de verdad

📘 Concepto: Un Map es una colección de pares clave → valor, como un objeto, pero diseñada específicamente para ese uso: las claves pueden ser de cualquier tipo (objetos, números, funciones…), mantiene el orden de inserción y conoce su tamaño.

const stock = new Map();

stock.set('manzanas', 50);       // añadir o sustituir
stock.set('peras', 20).set('uvas', 0);  // set devuelve el Map: se puede encadenar

console.log(stock.get('peras'));     // 20
console.log(stock.get('kiwis'));     // undefined
console.log(stock.has('uvas'));      // true
console.log(stock.size);             // 3

stock.delete('uvas');                // true si existía
console.log(stock.size);             // 2

// Crear un Map a partir de pares [clave, valor]
const capitales = new Map([
  ['España', 'Madrid'],
  ['Francia', 'París'],
  ['Portugal', 'Lisboa'],
]);
console.log(capitales.get('Francia'));  // París

1.1. Recorrer un Map

const capitales = new Map([['España', 'Madrid'], ['Francia', 'París'], ['Portugal', 'Lisboa']]);

// for...of devuelve pares [clave, valor], en orden de inserción
for (const [pais, capital] of capitales) {
  console.log(`${pais} → ${capital}`);
}
// España → Madrid
// Francia → París
// Portugal → Lisboa

console.log([...capitales.keys()]);    // ['España', 'Francia', 'Portugal']
console.log([...capitales.values()]);  // ['Madrid', 'París', 'Lisboa']

capitales.forEach((capital, pais) => console.log(`${capital} (${pais})`));  // ¡ojo: (valor, clave)!
// Madrid (España)
// París (Francia)
// Lisboa (Portugal)

// Convertir entre Map y objeto
const objeto = Object.fromEntries(capitales);
const otraVez = new Map(Object.entries(objeto));
console.log(objeto);  // { 'España': 'Madrid', Francia: 'París', Portugal: 'Lisboa' }

1.2. Claves de cualquier tipo

const ana = { nombre: 'Ana' };
const luis = { nombre: 'Luis' };

// Con un objeto, ambas claves se convertirían en el texto '[object Object]'
const malo = {};
malo[ana] = 'admin';
malo[luis] = 'lector';
console.log(Object.keys(malo));  // ['[object Object]']  ← ¡se han pisado!

// Con un Map, cada objeto es una clave distinta
const roles = new Map([[ana, 'admin'], [luis, 'lector']]);
console.log(roles.get(ana), roles.get(luis));  // admin lector

// Números, booleanos e incluso NaN son claves distintas de sus textos
const m = new Map([[1, 'número'], ['1', 'texto'], [NaN, 'no es número']]);
console.log(m.get(1), m.get('1'), m.get(NaN));  // número texto no es número

⚠️ Importante: Las claves objeto se comparan por referencia: roles.get({ nombre: 'Ana' }) devuelve undefined, porque es otro objeto aunque tenga el mismo contenido.

1.3. Map frente a objeto

Aspecto Objeto {} Map
Tipos de clave Cadenas y símbolos Cualquiera
Orden Enteros primero, luego inserción Siempre inserción
Tamaño Object.keys(obj).length map.size
Iterable directamente No (necesita Object.entries) Sí (for...of)
Claves heredadas Puede tener (toString…) Ninguna
Rendimiento con altas y bajas frecuentes Peor Optimizado para ello
Conversión a JSON Directa con JSON.stringify Hay que convertirlo antes (Object.fromEntries)
Sintaxis obj.clave, literal {} map.get(clave), map.set(clave, v)
Uso ideal Registros con campos fijos (un usuario, una configuración) Diccionarios dinámicos (caché, índices por id, contadores)

💡 Consejo: Regla rápida: si conoces los nombres de las propiedades al escribir el código, usa un objeto; si las claves llegan en tiempo de ejecución (ids, palabras, objetos), usa un Map.


2. getOrInsert y getOrInsertComputed (ES2026)

Un patrón muy repetido con Map es “si la clave no existe, inicialízala; después úsala”:

// Patrón clásico: contar palabras
const texto = 'el gato y el perro y el ratón';
const conteo = new Map();
for (const palabra of texto.split(' ')) {
  conteo.set(palabra, (conteo.get(palabra) ?? 0) + 1);
}
console.log(conteo);  // Map(5) { 'el' => 3, 'gato' => 1, 'y' => 2, 'perro' => 1, 'ratón' => 1 }

ES2026 añade dos métodos que expresan esta idea directamente:

Método Si la clave existe Si no existe
map.getOrInsert(clave, valorPorDefecto) Devuelve su valor Inserta valorPorDefecto y lo devuelve
map.getOrInsertComputed(clave, fn) Devuelve su valor (no llama a fn) Llama a fn(clave), inserta el resultado y lo devuelve
// Agrupar: obtener (o crear) el array del grupo y añadir
const alumnos = [
  { nombre: 'Ana', grupo: 'A' },
  { nombre: 'Luis', grupo: 'B' },
  { nombre: 'Eva', grupo: 'A' },
];
const porGrupo = new Map();
for (const { nombre, grupo } of alumnos) {
  porGrupo.getOrInsert(grupo, []).push(nombre);
}
console.log(porGrupo);  // Map(2) { 'A' => ['Ana', 'Eva'], 'B' => ['Luis'] }

// Contar palabras: getOrInsert devuelve el valor actual (o el 0 recién insertado)
const conteo = new Map();
for (const palabra of 'el gato y el perro'.split(' ')) {
  conteo.set(palabra, conteo.getOrInsert(palabra, 0) + 1);
}
console.log(conteo.get('el'));  // 2

⚠️ Importante: Con getOrInsert(grupo, []), el array vacío se crea en cada llamada, aunque la clave ya exista y se descarte. Cuando el valor por defecto es caro de calcular (una consulta, un cálculo pesado), usa getOrInsertComputed, que solo ejecuta la función cuando hace falta.

// Caché (memoización): el cálculo solo se hace la primera vez
const cache = new Map();
let calculos = 0;

function cuadradoLento(n) {
  return cache.getOrInsertComputed(n, (clave) => {
    calculos++;
    return clave * clave;  // imagina aquí algo costoso
  });
}

console.log(cuadradoLento(12), cuadradoLento(12), cuadradoLento(5));  // 144 144 25
console.log(`Cálculos reales: ${calculos}`);  // Cálculos reales: 2

⚠️ Importante: Estos métodos son muy recientes: están disponibles en Chrome y Edge 145+, Firefox 144+, Safari 26.2+ y Node.js 26. No existen en Node.js 24: si usas esa versión, utiliza el patrón clásico con has/get/set o el polyfill de core-js. También existen en WeakMap (sección 4).


3. Set: colecciones de valores únicos

📘 Concepto: Un Set es una colección de valores sin repetir. Si añades un valor que ya está, se ignora. Es como la lista de invitados de una fiesta: cada persona aparece una sola vez.

const etiquetas = new Set(['js', 'web']);
etiquetas.add('css');
etiquetas.add('js');  // ya estaba: no hace nada

console.log(etiquetas.size);         // 3
console.log(etiquetas.has('web'));   // true
etiquetas.delete('web');
console.log([...etiquetas]);         // ['js', 'css']

for (const etiqueta of etiquetas) {
  console.log(etiqueta);  // js, css (orden de inserción)
}

// El uso estrella: eliminar duplicados de un array
const visitas = [3, 1, 3, 2, 1, 3];
const unicos = [...new Set(visitas)];
console.log(unicos);  // [3, 1, 2]

// Contar valores distintos
console.log(new Set('mississippi').size);  // 4 (m, i, s, p)

💡 Consejo: set.has(valor) es mucho más rápido que array.includes(valor) en colecciones grandes: no recorre los elementos uno a uno. Si vas a comprobar muchas veces si algo “está en la lista”, conviértela en un Set.

3.1. Operaciones de conjuntos (ES2025)

Por fin, las operaciones de la teoría de conjuntos son métodos nativos. Todos devuelven un Set nuevo (o un booleano) y no modifican los originales:

flowchart LR
    A["A = {js, css, html}"] --- U["union: {js, css, html, python}"]
    B["B = {js, python}"] --- U
    A --- I["intersection: {js}"]
    B --- I
    A --- D["A.difference(B): {css, html}"]
    A --- S["symmetricDifference: {css, html, python}"]
const frontend = new Set(['js', 'css', 'html']);
const backend = new Set(['js', 'python', 'sql']);

console.log(frontend.union(backend));                // Set(5) { 'js', 'css', 'html', 'python', 'sql' }
console.log(frontend.intersection(backend));         // Set(1) { 'js' }
console.log(frontend.difference(backend));           // Set(2) { 'css', 'html' }
console.log(frontend.symmetricDifference(backend));  // Set(4) { 'css', 'html', 'python', 'sql' }

const basico = new Set(['html', 'css']);
console.log(basico.isSubsetOf(frontend));     // true  (todo basico está en frontend)
console.log(frontend.isSupersetOf(basico));   // true
console.log(basico.isDisjointFrom(backend));  // true  (no comparten nada)
Método Devuelve Pregunta
a.union(b) Set ¿Qué hay en a o en b?
a.intersection(b) Set ¿Qué hay en a y en b?
a.difference(b) Set ¿Qué hay en a pero no en b?
a.symmetricDifference(b) Set ¿Qué hay en uno solo de los dos?
a.isSubsetOf(b) boolean ¿Está todo a dentro de b?
a.isSupersetOf(b) boolean ¿Contiene a todo b?
a.isDisjointFrom(b) boolean ¿No tienen nada en común?

Un caso real: comparar los permisos de un usuario con los que exige una acción.

const permisosUsuario = new Set(['leer', 'escribir', 'comentar']);
const requeridos = new Set(['leer', 'escribir', 'borrar']);

if (requeridos.isSubsetOf(permisosUsuario)) {
  console.log('Acceso concedido');
} else {
  const faltan = requeridos.difference(permisosUsuario);
  console.log(`Faltan permisos: ${[...faltan].join(', ')}`);  // Faltan permisos: borrar
}

📘 Concepto: El argumento de estos métodos no tiene que ser un Set: basta con un objeto “parecido a un Set” (con size, has y keys), como un Map. Un array no sirve directamente: conviértelo antes con new Set(array).


4. WeakMap y WeakSet

📘 Concepto: WeakMap y WeakSet guardan sus claves de forma débil: si un objeto usado como clave deja de estar referenciado en el resto del programa, el recolector de basura puede eliminarlo de la memoria, y su entrada desaparece de la colección automáticamente.

Con un Map normal, mientras el objeto esté en el Map, nunca se libera aunque nadie más lo use: es una fuga de memoria típica en cachés y en datos asociados a elementos del DOM.

  Map / Set WeakMap / WeakSet
Claves Cualquier valor Solo objetos (y símbolos no registrados)
Impide liberar la clave Sí No
Iterable, size, clear Sí No (su contenido puede cambiar en cualquier momento)
Métodos Todos WeakMap: get, set, has, delete, getOrInsert, getOrInsertComputed · WeakSet: add, has, delete

4.1. Ejemplo: datos asociados y caché

// Caché de resultados por objeto: se libera sola cuando el objeto deja de usarse
const cacheEstadisticas = new WeakMap();

function estadisticas(lista) {
  if (cacheEstadisticas.has(lista)) return cacheEstadisticas.get(lista);
  console.log('Calculando...');
  const total = lista.valores.reduce((s, v) => s + v, 0);
  const resultado = { total, media: total / lista.valores.length };
  cacheEstadisticas.set(lista, resultado);
  return resultado;
}

let ventas = { valores: [120, 80, 100] };
console.log(estadisticas(ventas));  // Calculando... → { total: 300, media: 100 }
console.log(estadisticas(ventas));  // { total: 300, media: 100 } (desde la caché)

ventas = null;  // el objeto ya no se usa: su entrada en la caché podrá liberarse
// WeakSet: marcar objetos sin modificarlos
const procesados = new WeakSet();

function procesarPedido(pedido) {
  if (procesados.has(pedido)) {
    return `Pedido ${pedido.id} ya procesado`;
  }
  procesados.add(pedido);
  return `Procesando pedido ${pedido.id}`;
}

const p = { id: 7 };
console.log(procesarPedido(p));  // Procesando pedido 7
console.log(procesarPedido(p));  // Pedido 7 ya procesado

💡 Consejo: Antes de existir los campos #privados (módulo 09), un WeakMap era la forma de guardar datos privados de cada instancia: privados.set(this, { saldo: 0 }). Hoy se usa sobre todo para asociar información a objetos que no controlas (elementos del DOM, objetos de una biblioteca) sin modificarlos ni impedir que se liberen.

4.2. WeakRef (mención)

WeakRef crea una referencia débil a un único objeto: const ref = new WeakRef(obj), y ref.deref() devuelve el objeto o undefined si ya fue liberado. Junto con FinalizationRegistry (que avisa cuando un objeto se libera), son herramientas avanzadas para cachés grandes. MDN recomienda evitarlas salvo necesidad real, porque el momento en que actúa el recolector de basura es impredecible.


5. Symbol

📘 Concepto: Un Symbol es un tipo primitivo cuyo valor es único e irrepetible. Se usa sobre todo como clave de propiedad que no puede chocar con ninguna otra.

const id1 = Symbol('id');
const id2 = Symbol('id');
console.log(id1 === id2);      // false (la descripción no importa: cada símbolo es único)
console.log(id1.description);  // id
console.log(typeof id1);       // symbol

// Como clave de propiedad: no aparece al recorrer ni en JSON
const ID = Symbol('id');
const usuario = { nombre: 'Ana', [ID]: 'u-123' };
console.log(usuario[ID]);              // u-123
console.log(Object.keys(usuario));     // ['nombre']
console.log(JSON.stringify(usuario));  // {"nombre":"Ana"}
console.log(Object.getOwnPropertySymbols(usuario));  // [Symbol(id)]

5.1. Símbolos globales: Symbol.for

Symbol.for(clave) busca en un registro global: con la misma clave devuelve siempre el mismo símbolo, incluso entre módulos o iframes.

const a = Symbol.for('app.version');
const b = Symbol.for('app.version');
console.log(a === b);             // true
console.log(Symbol.keyFor(a));    // app.version

5.2. Símbolos conocidos (well-known symbols)

El lenguaje define símbolos especiales que actúan como ganchos: si tu objeto define una propiedad con ese símbolo, JavaScript la usará en ciertas operaciones.

Símbolo JavaScript lo usa al…
Symbol.iterator Recorrer con for...of, spread, desestructuración (sección 6)
Symbol.asyncIterator Recorrer con for await...of (sección 9)
Symbol.toPrimitive Convertir a número o texto (módulo 09)
Symbol.toStringTag Generar el texto de Object.prototype.toString ([object ...])
Symbol.hasInstance Evaluar instanceof
class Coleccion {
  get [Symbol.toStringTag]() {
    return 'Coleccion';
  }
}
console.log(Object.prototype.toString.call(new Coleccion()));  // [object Coleccion]

6. El protocolo de iteración

¿Por qué for...of funciona con arrays, cadenas, Map y Set, pero no con objetos normales? Porque todos ellos cumplen el protocolo de iteración.

📘 Concepto: Un objeto es iterable si tiene un método [Symbol.iterator]() que devuelve un iterador. Un iterador es un objeto con un método next() que, en cada llamada, devuelve { value, done }: el siguiente valor y si ya se ha terminado.

const letras = ['a', 'b'];
const iterador = letras[Symbol.iterator]();

console.log(iterador.next());  // { value: 'a', done: false }
console.log(iterador.next());  // { value: 'b', done: false }
console.log(iterador.next());  // { value: undefined, done: true }
sequenceDiagram
    participant F as for...of
    participant O as Objeto iterable
    participant I as Iterador
    F->>O: [Symbol.iterator]()
    O-->>F: iterador
    loop Hasta done: true
        F->>I: next()
        I-->>F: { value, done }
    end

6.1. Crear un iterable propio

class Rango {
  constructor(desde, hasta, paso = 1) {
    this.desde = desde;
    this.hasta = hasta;
    this.paso = paso;
  }

  [Symbol.iterator]() {
    let actual = this.desde;
    const { hasta, paso } = this;
    return {
      next() {
        if (actual <= hasta) {
          const value = actual;
          actual += paso;
          return { value, done: false };
        }
        return { value: undefined, done: true };
      },
    };
  }
}

const rango = new Rango(1, 10, 3);
for (const n of rango) console.log(n);  // 1, 4, 7, 10

// Al ser iterable, funciona con todo lo que acepta iterables
console.log([...rango]);                   // [1, 4, 7, 10]
console.log(Math.max(...rango));           // 10
const [primero, segundo] = rango;
console.log(primero, segundo);             // 1 4
console.log(Array.from(rango, (n) => n * 2));  // [2, 8, 14, 20]
console.log(new Set(new Rango(1, 3)));     // Set(3) { 1, 2, 3 }

💡 Consejo: Escribir iteradores a mano con next() es engorroso. En la práctica se usan generadores, que hacen todo esto por ti.


7. Generadores

📘 Concepto: Una función generadora (function*) es una función que puede pausarse y reanudarse. Cada yield entrega un valor y congela la función en ese punto hasta que alguien pida el siguiente. Al llamarla no se ejecuta: devuelve un generador, que es a la vez iterador e iterable.

Piensa en un dispensador de números de turno de la carnicería: no imprime todos los números de golpe, sino que te da uno cada vez que tiras del papel.

function* contarHasta3() {
  console.log('Empiezo');
  yield 1;
  console.log('Sigo');
  yield 2;
  yield 3;
  console.log('Termino');
}

const gen = contarHasta3();  // aún no se ha ejecutado nada
console.log(gen.next());     // Empiezo → { value: 1, done: false }
console.log(gen.next());     // Sigo → { value: 2, done: false }
console.log(gen.next());     // { value: 3, done: false }
console.log(gen.next());     // Termino → { value: undefined, done: true }

// Y como es iterable, lo normal es recorrerlo con for...of o spread
console.log([...contarHasta3()]);  // (mensajes) → [1, 2, 3]

7.1. El Rango con un generador

class Rango {
  constructor(desde, hasta, paso = 1) {
    Object.assign(this, { desde, hasta, paso });
  }

  // Un método generador como Symbol.iterator: 4 líneas en vez de 15
  *[Symbol.iterator]() {
    for (let n = this.desde; n <= this.hasta; n += this.paso) {
      yield n;
    }
  }
}
console.log([...new Rango(0, 20, 5)]);  // [0, 5, 10, 15, 20]

7.2. yield*: delegar en otro iterable

function* vocales() {
  yield 'a';
  yield 'e';
}

function* letras() {
  yield* vocales();       // entrega todos los valores de otro generador
  yield* ['x', 'y'];      // o de cualquier iterable
  yield 'z';
}
console.log([...letras()]);  // ['a', 'e', 'x', 'y', 'z']

// Muy útil para recorrer estructuras recursivas, como un árbol de carpetas
const arbol = {
  nombre: 'src',
  hijos: [
    { nombre: 'index.js' },
    { nombre: 'utils', hijos: [{ nombre: 'fecha.js' }, { nombre: 'texto.js' }] },
  ],
};

function* recorrer(nodo, ruta = '') {
  const actual = `${ruta}/${nodo.nombre}`;
  yield actual;
  for (const hijo of nodo.hijos ?? []) {
    yield* recorrer(hijo, actual);
  }
}
console.log([...recorrer(arbol)]);
// ['/src', '/src/index.js', '/src/utils', '/src/utils/fecha.js', '/src/utils/texto.js']

7.3. Generadores infinitos

Como los valores se calculan bajo demanda, un generador puede no terminar nunca: solo se calcula lo que se pide.

function* generarIds(prefijo = 'id') {
  let n = 1;
  while (true) {
    yield `${prefijo}-${String(n++).padStart(4, '0')}`;
  }
}

const ids = generarIds('pedido');
console.log(ids.next().value);  // pedido-0001
console.log(ids.next().value);  // pedido-0002
console.log(ids.next().value);  // pedido-0003

⚠️ Importante: Nunca hagas [...generarIds()] ni Array.from de un generador infinito: intentaría crear un array infinito y bloquearía el programa. Toma solo los que necesites con take (sección 8) o con un bucle con break.

7.4. return y la finalización

function* conReturn() {
  yield 1;
  return 'fin';  // termina el generador: este valor NO lo ve for...of
  yield 2;       // nunca se alcanza
}
const g = conReturn();
console.log(g.next());  // { value: 1, done: false }
console.log(g.next());  // { value: 'fin', done: true }
console.log([...conReturn()]);  // [1]

// gen.return(valor) termina un generador desde fuera
function* generarTurnos() {
  try {
    let n = 1;
    while (true) yield n++;
  } finally {
    console.log('Dispensador cerrado');  // se ejecuta al terminar, también con break
  }
}
const turnos = generarTurnos();
console.log(turnos.next().value);   // 1
console.log(turnos.return('cerrado'));  // Dispensador cerrado → { value: 'cerrado', done: true }
console.log(turnos.next());         // { value: undefined, done: true }

📘 Concepto: Si sales de un for...of con break, return o una excepción, JavaScript llama automáticamente a return() del iterador. Por eso los bloques finally de un generador sirven para liberar recursos aunque el recorrido se corte antes de tiempo.


8. Iterator helpers (ES2025)

Hasta ES2025, para usar map o filter sobre un iterador había que convertirlo antes en array ([...iterador].map(...)), lo que calcula y guarda todos los valores. Los iterator helpers añaden estos métodos directamente a los iteradores, con evaluación perezosa.

Método Devuelve Descripción
map(fn) Iterador Transforma cada valor
filter(fn) Iterador Deja pasar los que cumplen
take(n) Iterador Solo los n primeros (¡imprescindible con infinitos!)
drop(n) Iterador Salta los n primeros
flatMap(fn) Iterador Transforma y aplana (la función debe devolver un iterable o iterador)
reduce(fn, inicial) Valor Reduce a un único valor
toArray() Array Recoge todos los valores
forEach(fn) undefined Ejecuta fn por cada valor
some(fn) / every(fn) / find(fn) Booleano / valor Como en los arrays; se detienen en cuanto saben la respuesta
Iterator.from(obj) Iterador Convierte un iterable (o un objeto con next) en un iterador con estos métodos
const precios = new Map([['pan', 1.2], ['aceite', 8.75], ['leche', 0.95], ['queso', 6.4]]);

// map.values() devuelve un iterador: ya tiene los helpers
const caros = precios
  .values()
  .filter((p) => p > 1)
  .map((p) => `${p.toFixed(2)} €`)
  .toArray();
console.log(caros);  // ['1.20 €', '8.75 €', '6.40 €']

const total = precios.values().reduce((s, p) => s + p, 0);
console.log(total.toFixed(2));  // 17.30

// Iterator.from: dar helpers a cualquier iterable (un Set, una cadena, tu Rango...)
const iniciales = Iterator.from('hola mundo').filter((c) => c !== ' ').take(4).toArray();
console.log(iniciales);  // ['h', 'o', 'l', 'a']

⚠️ Importante: Los arrays no tienen take ni drop (y sus map/filter son los de siempre, que crean arrays). Para usar los helpers perezosos sobre un array, pide su iterador: array.values().map(...).

8.1. Evaluación perezosa

📘 Concepto: Evaluación perezosa significa que cada valor recorre toda la cadena de operaciones uno a uno, y solo cuando se pide. No se crean arrays intermedios, y si solo necesitas 3 resultados, solo se calculan los valores imprescindibles para obtenerlos.

function* naturales() {
  let n = 1;
  while (true) {
    console.log(`  genero ${n}`);
    yield n++;
  }
}

const resultado = naturales()
  .filter((n) => n % 2 === 0)
  .map((n) => n * 10)
  .take(2);

console.log('Nada calculado todavía');
console.log(resultado.toArray());
// Nada calculado todavía
//   genero 1
//   genero 2
//   genero 3
//   genero 4
// [20, 40]

8.2. Ejemplo: números primos y Fibonacci

function* naturalesDesde(inicio) {
  for (let n = inicio; ; n++) yield n;
}

const esPrimo = (n) => {
  if (n < 2) return false;
  for (let d = 2; d * d <= n; d++) if (n % d === 0) return false;
  return true;
};

// Los 10 primeros primos (de una secuencia infinita)
console.log(naturalesDesde(2).filter(esPrimo).take(10).toArray());
// [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]

// El primer primo mayor que 1000
console.log(naturalesDesde(1001).find(esPrimo));  // 1009

// Fibonacci infinito
function* fibonacci() {
  let [a, b] = [0, 1];
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}

console.log(fibonacci().take(10).toArray());  // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

// Los Fibonacci pares, saltando los 2 primeros, hasta tener 4
console.log(fibonacci().filter((n) => n % 2 === 0).drop(2).take(4).toArray());  // [8, 34, 144, 610]

// Suma de los Fibonacci menores de 100: no existe un helper "takeWhile",
// así que para "mientras se cumpla" usamos for...of con break
let suma = 0;
for (const n of fibonacci()) {
  if (n >= 100) break;
  suma += n;
}
console.log(suma);  // 232

// flatMap: cada número produce varios valores
console.log(Iterator.from([1, 2, 3]).flatMap((n) => [n, -n]).toArray());  // [1, -1, 2, -2, 3, -3]

💡 Consejo: Los iterator helpers brillan con secuencias grandes o infinitas y con datos que se van generando (líneas de un fichero, resultados paginados). Para arrays pequeños, los métodos de array de siempre son igual de buenos y más conocidos.

8.3. Iterator.concat (ES2026)

Iterator.concat(...iterables) crea un iterador que recorre varios iterables uno detrás de otro, también de forma perezosa y sin crear un array intermedio:

const pendientes = ['factura-1', 'factura-2'];
const urgentes = new Set(['aviso-9']);
function* nuevos() {
  yield 'pedido-77';
  yield 'pedido-78';
}

const cola = Iterator.concat(urgentes, pendientes, nuevos());
console.log(cola.toArray());  // ['aviso-9', 'factura-1', 'factura-2', 'pedido-77', 'pedido-78']

// Al devolver un iterador, admite todos los helpers
const todo = Iterator.concat([1, 2], new Set([10, 11, 12])).map((n) => n * 2).toArray();
console.log(todo);  // [2, 4, 20, 22, 24]

⚠️ Importante: Iterator.concat está disponible en Chrome y Edge 146+, Firefox 147+, Safari 26.4+ y Node.js 26, pero no en Node.js 24. En Node 24 puedes conseguir lo mismo con un generador: function* concatenar(...its) { for (const it of its) yield* it; }.


9. Generadores asíncronos y for await...of (adelanto)

Un generador asíncrono (async function*) puede usar await dentro y entrega sus valores como promesas. Se recorre con for await...of. Es ideal para datos que llegan poco a poco: páginas de una API, líneas de un fichero grande, mensajes de un stream.

const esperar = (ms) => new Promise((resolver) => setTimeout(resolver, ms));

// Simula una API paginada (en el módulo 17 usarás fetch de verdad)
async function* obtenerPaginas(totalPaginas) {
  for (let pagina = 1; pagina <= totalPaginas; pagina++) {
    await esperar(50);  // simula la espera de red
    yield { pagina, elementos: [`item-${pagina}a`, `item-${pagina}b`] };
  }
}

for await (const { pagina, elementos } of obtenerPaginas(3)) {
  console.log(`Página ${pagina}: ${elementos.join(', ')}`);
}
// Página 1: item-1a, item-1b
// Página 2: item-2a, item-2b
// Página 3: item-3a, item-3b

// Array.fromAsync (ES2026, módulo 06) recoge un iterable asíncrono en un array
const todas = await Array.fromAsync(obtenerPaginas(2));
console.log(todas.flatMap((p) => p.elementos));  // ['item-1a', 'item-1b', 'item-2a', 'item-2b']

📘 Concepto: Profundizarás en promesas, async/await y for await...of en el módulo 12 (Asincronía). De momento, quédate con la idea: lo mismo que un generador normal, pero cada valor puede tardar en llegar.


10. Ejercicios

Ejercicio 1: Agenda con Map

Crea un Map cuyas claves sean números de teléfono (texto) y los valores objetos { nombre, email }. Escribe funciones para añadir, buscar, eliminar y listar los contactos ordenados por nombre (convierte con [...map.values()] y toSorted). Finalmente, convierte el Map en un objeto y en JSON.

Ejercicio 2: Frecuencia de palabras

Escribe frecuencias(texto) que devuelva un Map palabra → número de apariciones, ignorando mayúsculas y signos de puntuación (usa una regex con \p{L} del módulo 08). Hazlo con getOrInsert si usas Node 26 (o con get/set en Node 24) y muestra las 5 palabras más frecuentes.

Ejercicio 3: Comparar listas con Set

Tienes dos arrays con los emails de los asistentes a dos eventos. Usa los métodos de Set de ES2025 para obtener: quiénes fueron a ambos, quiénes solo al primero, quiénes a uno solo de los dos y el total de personas distintas. Comprueba con isDisjointFrom si hay algún asistente en común.

Ejercicio 4: Caché con WeakMap

Escribe memoizarPorObjeto(fn) que devuelva una versión de fn(objeto) que guarde el resultado en un WeakMap, de modo que la segunda llamada con el mismo objeto no recalcule. Cuenta cuántas veces se ejecuta realmente fn y explica en un comentario por qué un Map provocaría una fuga de memoria.

Ejercicio 5: Iterable propio

Crea una clase Baraja que contenga las 40 cartas de la baraja española ('1 de oros'… 'rey de bastos') y sea iterable con un método generador *[Symbol.iterator](). Añade un método *repartir(n) que entregue cartas aleatorias sin repetir. Usa spread, desestructuración y for...of con ella.

Ejercicio 6: Generadores de secuencias

Escribe los generadores pares(), potenciasDe(base) y ciclo(array) (que repite los elementos indefinidamente: a, b, c, a, b, c...). Combínalos con iterator helpers para obtener: los 5 primeros pares mayores que 100, las potencias de 2 menores que 1000 y los 7 primeros elementos de ciclo(['rojo', 'ámbar', 'verde']).

Ejercicio 7: Procesado perezoso de un log

Crea un generador lineasLog() que produzca 1 000 000 de líneas simuladas como '2026-10-04 INFO Mensaje 1' (con algún ERROR cada cierto número). Usando solo iterator helpers (sin crear arrays intermedios), obtén las 3 primeras líneas de error y el número total de errores en las 10 000 primeras líneas. Añade un contador para demostrar cuántas líneas se han generado realmente en cada caso.

Ejercicio 8: Paginación asíncrona

Partiendo de obtenerPaginas de la sección 9, escribe un generador asíncrono todosLosElementos() que, usando yield* sobre los elementos de cada página, entregue los elementos uno a uno. Recórrelo con for await...of y detente con break al encontrar el elemento 'item-2b'. Comprueba (con un console.log en un finally) que el generador se cierra correctamente.


Resumen

Concepto Sintaxis
Map new Map([[k, v]]), set, get, has, delete, size, for (const [k, v] of map)
ES2026 map.getOrInsert(k, valor), map.getOrInsertComputed(k, (k) => valor)
Set new Set(iterable), add, has, delete, size, [...new Set(array)]
Conjuntos (ES2025) union, intersection, difference, symmetricDifference, isSubsetOf, isSupersetOf, isDisjointFrom
Referencias débiles WeakMap, WeakSet (claves objeto, no iterables), WeakRef
Symbol Symbol('desc'), Symbol.for('clave'), Symbol.iterator, Symbol.toPrimitive
Iterable Objeto con [Symbol.iterator]() que devuelve { next() { return { value, done }; } }
Generador function* gen() { yield 1; yield* otro(); return fin; }
Método generador *[Symbol.iterator]() { ... }
Control gen.next(), gen.return(v), finally para limpiar
Iterator helpers (ES2025) map, filter, take, drop, flatMap, reduce, toArray, forEach, some, every, find, Iterator.from
Concatenar (ES2026) Iterator.concat(it1, it2, ...)
Asíncrono async function*, for await (const x of gen()), Array.fromAsync