Set y Map en JavaScript
En esta lección
Los arrays y los objetos resuelven casi todo, pero hay dos problemas en los que se quedan cortos: guardar valores sin repetir y asociar datos a claves que no son texto. Para eso JavaScript tiene dos estructuras de datos específicas: Set (un conjunto de valores únicos) y Map (un diccionario clave → valor de verdad). En esta lección verás cómo usarlas, cuándo convienen más que un array o un objeto y cómo agrupar datos con Object.groupBy.
Antes conviene que domines los arrays, los objetos y el operador spread de desestructuración y spread.
Set: una colección sin repetidos
Un Set guarda valores únicos. Si añades uno que ya está, simplemente no pasa nada:
const etiquetas = new Set();
etiquetas.add("js");
etiquetas.add("css");
etiquetas.add("js"); // ya estaba: se ignora
console.log(etiquetas); // Set(2) { 'js', 'css' }
console.log(etiquetas.size); // 2
Fíjate en que el tamaño es size, no length. Puedes crearlo ya con datos pasando un array (o cualquier cosa recorrible), y add se puede encadenar:
const letras = new Set(["a", "b"]).add("c").add("a");
console.log(letras); // Set(3) { 'a', 'b', 'c' }
Comprobar, borrar y vaciar
etiquetas.has("css"); // true
etiquetas.has("html"); // false
etiquetas.delete("css"); // true: estaba y lo borra
etiquetas.delete("java"); // false: no estaba
etiquetas.clear(); // lo vacía entero
¿Por qué no usar array.includes? Porque has es mucho más rápido con muchos datos: el Set está organizado internamente para encontrar un valor sin recorrerlos todos. Con 10 elementos da igual; con 100 000, se nota.
Recorrer un Set
Un Set mantiene el orden de inserción y se recorre con for...of o forEach:
for (const letra of letras) {
console.log(letra); // a, b, c
}
Lo que no tiene es acceso por posición: letras[0] es undefined. Si lo necesitas, conviértelo en array.
Quitar duplicados de un array
Es el uso más frecuente, y cabe en una línea:
const visitas = ["ana", "luis", "ana", "eva", "luis", "ana"];
const unicos = [...new Set(visitas)];
console.log(unicos); // [ 'ana', 'luis', 'eva' ]
console.log(new Set(visitas).size); // 3 usuarios distintos
new Set(visitas) elimina los repetidos y [...] lo vuelve a convertir en array. También vale Array.from(new Set(visitas)).
Cómo decide que dos valores son iguales
Compara como === (salvo que considera NaN igual a NaN). Así que 1 y "1" son distintos, y dos objetos con el mismo contenido también:
new Set([1, "1", 1]).size; // 2
new Set([{ id: 1 }, { id: 1 }]).size; // 2: son dos objetos diferentes
Para quitar objetos repetidos por un campo, usa un Map con ese campo como clave, o un Set de ids.
Operaciones de conjuntos
Intersección, unión y diferencia se pueden hacer con spread y filter:
const a = new Set(["js", "css", "html"]);
const b = new Set(["js", "python"]);
const comunes = new Set([...a].filter((x) => b.has(x))); // Set(1) { 'js' }
Los navegadores y Node recientes ya incluyen métodos propios: a.intersection(b), a.union(b), a.difference(b), a.isSubsetOf(b)… Son de ES2025, así que comprueba que tu entorno los soporta antes de usarlos.
Map: un diccionario de verdad
Un Map guarda pares clave → valor, como un objeto, pero pensado precisamente para eso:
const stock = new Map();
stock.set("teclado", 12);
stock.set("raton", 0).set("taza", 30); // set también se encadena
stock.get("taza"); // 30
stock.get("silla"); // undefined
stock.has("raton"); // true
stock.size; // 3
stock.set("taza", 29); // si la clave existe, sustituye el valor
stock.delete("raton");
console.log(stock); // Map(2) { 'teclado' => 12, 'taza' => 29 }
Se puede crear con datos pasando un array de pares [clave, valor]:
const precios = new Map([
["cafe", 1.5],
["te", 1.2],
]);
Claves de cualquier tipo
Esta es la gran diferencia con un objeto. En un objeto, las claves se convierten a texto. En un Map, una clave puede ser un número, un booleano, un objeto o incluso una función:
const ana = { nombre: "Ana" };
const luis = { nombre: "Luis" };
const puntos = new Map();
puntos.set(ana, 120);
puntos.set(luis, 80);
puntos.get(ana); // 120
Con un objeto normal, las dos claves se convertirían en el texto "[object Object]" y la segunda pisaría a la primera:
const mal = {};
mal[ana] = 120;
mal[luis] = 80;
console.log(mal); // { '[object Object]': 80 }
Y 1 y "1" son claves distintas en un Map, pero la misma en un objeto.
Recorrer un Map
Un Map se recorre en orden de inserción, y cada vuelta te da un par que puedes desestructurar:
for (const [producto, precio] of precios) {
console.log(`${producto}: ${precio} €`);
}
// cafe: 1.5 €
// te: 1.2 €
[...precios.keys()]; // [ 'cafe', 'te' ]
[...precios.values()]; // [ 1.5, 1.2 ]
[...precios.entries()]; // [ [ 'cafe', 1.5 ], [ 'te', 1.2 ] ]
precios.forEach((valor, clave) => console.log(clave, valor));
Cuidado con forEach: recibe primero el valor y después la clave.
Un caso real: contar
Contar cuántas veces aparece cada palabra es un uso típico:
const palabras = "el gato y el perro y el loro".split(" ");
const frecuencia = new Map();
for (const palabra of palabras) {
frecuencia.set(palabra, (frecuencia.get(palabra) ?? 0) + 1);
}
console.log(frecuencia);
// Map(5) { 'el' => 3, 'gato' => 1, 'y' => 2, 'perro' => 1, 'loro' => 1 }
Convertir entre objeto y Map
Object.entries convierte un objeto en un array de pares, justo lo que acepta new Map. Object.fromEntries hace el camino inverso:
const obj = { cafe: 1.5, te: 1.2 };
const mapa = new Map(Object.entries(obj)); // Map(2) { 'cafe' => 1.5, 'te' => 1.2 }
const deVuelta = Object.fromEntries(mapa); // { cafe: 1.5, te: 1.2 }
Object.fromEntries también sirve para transformar un objeto sin Map de por medio:
const rebajado = Object.fromEntries(
Object.entries(obj).map(([producto, precio]) => [producto, precio * 0.9])
);
// { cafe: 1.35, te: 1.08 }
Cuidado:
JSON.stringifyno sabe convertir unMap: devuelve"{}". Para guardarlo o enviarlo, conviértelo antes conObject.fromEntries(mapa)(si las claves son texto) o con[...mapa].
Map frente a objeto
Objeto {} | Map | |
|---|---|---|
| Claves | Solo texto (y símbolos) | Cualquier tipo |
| Tamaño | Object.keys(o).length | mapa.size |
| Recorrer | Object.entries(o) | for...of directo |
| Orden | Casi siempre el de inserción (los números van primero) | Siempre el de inserción |
| Claves heredadas | Sí: "toString" in {} es true | No, empieza vacío |
| JSON | Directo | Hay que convertirlo |
| Añadir y borrar a menudo | Más lento | Optimizado |
Usa un objeto para representar una “cosa” con propiedades fijas y conocidas (un usuario, un producto, una configuración) y para datos que vas a convertir a JSON.
Usa un Map cuando es una colección de pares que crece y cambia, cuando las claves vienen de datos (ids, palabras, nombres de usuario) o cuando las claves no son texto.
Nota: existen también
WeakMapyWeakSet. Solo admiten objetos como claves y no impiden que el recolector de basura los elimine cuando ya nadie más los usa. No se pueden recorrer ni tienensize. Sirven para asociar datos extra a objetos que no controlas (por ejemplo, elementos del DOM) sin provocar fugas de memoria. Al empezar, rara vez los necesitarás.
Agrupar con Object.groupBy y Map.groupBy
Agrupar una lista por un criterio es tan habitual que desde ES2024 existe un método para ello. Recibe el array y una función que devuelve la clave del grupo de cada elemento:
const productos = [
{ nombre: "Teclado", categoria: "informatica", precio: 45 },
{ nombre: "Taza", categoria: "hogar", precio: 8 },
{ nombre: "Ratón", categoria: "informatica", precio: 20 },
];
const porCategoria = Object.groupBy(productos, (p) => p.categoria);
// {
// informatica: [ { nombre: 'Teclado', ... }, { nombre: 'Ratón', ... } ],
// hogar: [ { nombre: 'Taza', ... } ]
// }
La clave puede ser cualquier cálculo:
Object.groupBy(productos, (p) => (p.precio >= 20 ? "caros" : "baratos"));
// { caros: [ Teclado, Ratón ], baratos: [ Taza ] }
Map.groupBy hace lo mismo pero devuelve un Map, útil si las claves no son texto:
const grupos = Map.groupBy(productos, (p) => p.categoria);
grupos.get("hogar"); // [ { nombre: 'Taza', categoria: 'hogar', precio: 8 } ]
Antes había que hacerlo con reduce, como viste en métodos de arrays. Si aparece [Object: null prototype] al mostrar el resultado en Node, es normal: el objeto se crea sin propiedades heredadas.
Errores frecuentes
- Usar
lengthen vez desize:set.lengthymap.lengthsonundefined. - Leer un Map con corchetes:
mapa["cafe"]no funciona; usamapa.get("cafe"). Ymapa.cafe = 2crea una propiedad normal, no una entrada del Map. - Esperar que el Set elimine objetos “iguales”: compara referencias, no contenido.
- Guardar un Map con
JSON.stringifydirectamente: obtienes{}. - Buscar con objetos distintos:
puntos.get({ nombre: "Ana" })daundefinedaunque el contenido coincida; tiene que ser el mismo objeto. - Confundir el orden en
forEachde un Map: primero valor, luego clave.
Resumen
| Set | Map |
|---|---|
new Set(array) | new Map([[clave, valor], ...]) |
add(valor) | set(clave, valor) |
has(valor) | has(clave) / get(clave) |
delete(valor) | delete(clave) |
size, clear() | size, clear() |
for (const v of set) | for (const [k, v] of map) |
[...new Set(arr)] quita duplicados | Object.entries / Object.fromEntries para convertir |
Object.groupBy(lista, fn)agrupa en un objeto;Map.groupByen unMap.- Objeto para “cosas” con propiedades fijas;
Mappara colecciones de pares que cambian.
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.