Módulo 11: Colecciones, iteradores y generadores
Objetivos del módulo
- Usar
Mappara diccionarios con claves de cualquier tipo y saber cuándo preferirlo a un objeto - Aprovechar
getOrInsertygetOrInsertComputed(ES2026) para contar, agrupar y cachear - Usar
Setpara valores únicos y las operaciones de conjuntos de ES2025 (union,intersection…) - Entender para qué sirven
WeakMap,WeakSetyWeakRef - 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*,yieldyyield*, 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
Mapes 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' })devuelveundefined, 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), usagetOrInsertComputed, 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/seto el polyfill decore-js. También existen enWeakMap(sección 4).
3. Set: colecciones de valores únicos
📘 Concepto: Un
Setes 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 quearray.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 unSet.
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” (consize,hasykeys), como unMap. Un array no sirve directamente: conviértelo antes connew Set(array).
4. WeakMap y WeakSet
📘 Concepto:
WeakMapyWeakSetguardan 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), unWeakMapera 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
Symboles 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étodonext()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. Cadayieldentrega 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()]niArray.fromde un generador infinito: intentaría crear un array infinito y bloquearía el programa. Toma solo los que necesites contake(sección 8) o con un bucle conbreak.
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...ofconbreak,returno una excepción, JavaScript llama automáticamente areturn()del iterador. Por eso los bloquesfinallyde 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
takenidrop(y susmap/filterson 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.concatestá 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/awaityfor await...ofen 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 |