Ya conoces el array (lista ordenada por índice) y el objeto (pares clave-valor). Cubren casi todo, pero hay dos trabajos para los que se quedan cortos: garantizar que no hay duplicados y tener un diccionario clave-valor con tamaño, orden y claves de cualquier tipo. Para eso JavaScript trae dos colecciones más: Set y Map.
Trabajaremos con un registro de partidas donde el mismo héroe aparece varias veces. Es el caso natural para preguntarse “¿cuáles distintos?” (Set) y “¿cuántos de cada uno?” (Map).
Set: una colección sin duplicados#
Un Set guarda valores, pero cada valor una sola vez. Si intentas meter uno repetido, se
ignora. Lo construyes vacío y vas añadiendo, o de golpe desde un array.
// Vacío y añadiendo con add. El segundo 'Daño' se ignora: ya estaba.
const roles = new Set();
roles.add('Daño');
roles.add('Apoyo');
// duplicado: no entra
roles.add('Daño');
// 2 (tamaño: se llama size, no length)
console.log(roles.size);
// true (¿está? respuesta inmediata)
console.log(roles.has('Apoyo'));
// quita un valor
roles.delete('Daño');
// 1
console.log(roles.size);Dos cosas frente al array: el tamaño es .size (no .length) y preguntar si un valor está es
.has(valor), que es inmediato —no mira los elementos uno a uno como haría array.includes:
va directo al valor—.
Un Set no tiene índices: no es para acceder por posición, es para pertenencia y unicidad.
Para recorrerlo o volverlo array, usas for...of o spread:
const roles = new Set(['Daño', 'Apoyo', 'Tanque']);
for (const rol of roles) {
// Daño, Apoyo, Tanque (en orden de inserción)
console.log(rol);
}
// ['Daño', 'Apoyo', 'Tanque']
const comoArray = [...roles];Unicidad: quitar duplicados de un array#
El uso estrella de Set: deduplicar. Construir un Set desde un array descarta los
repetidos; sacarlo de nuevo con spread te devuelve un array sin duplicados. Es un patrón que ya
viste de pasada y conviene fijar:
const nombres = ['Tracer', 'Mercy', 'Tracer', 'Ana', 'Mercy', 'Tracer'];
// new Set quita las repeticiones; [...] lo vuelve a array.
const unicos = [...new Set(nombres)];
// unicos → ['Tracer', 'Mercy', 'Ana']
// 3
console.log(unicos.length);Un detalle sobre NaN: Set lo trata como igual a sí mismo aunque en JS NaN !== NaN. Por eso
new Set([NaN, NaN]) guarda un único NaN.
// NaN duplicado: solo entra uno
const s = new Set([NaN, NaN]);
// 1
console.log(s.size);Map: un diccionario clave-valor de verdad#
Un objeto plano ya asocia claves a valores. ¿Para qué un Map? Porque un Map es una
estructura pensada para eso, con cosas que el objeto no da bien. Se maneja con métodos,
no con corchetes:
const conteo = new Map();
// guardar (clave, valor)
conteo.set('Tracer', 3);
conteo.set('Mercy', 2);
// 3 (leer)
console.log(conteo.get('Tracer'));
// true (¿existe la clave?)
console.log(conteo.has('Mercy'));
// 2 (cuántas claves, directo)
console.log(conteo.size);
// borrar una clave
conteo.delete('Mercy');El patrón de contar es get el valor actual (o 0 si no existe) y set el incrementado:
const partidas = ['Tracer', 'Mercy', 'Tracer', 'Ana', 'Tracer'];
const conteo = new Map();
for (const heroe of partidas) {
// Si la clave aún no existe, get devuelve undefined → el || 0 arranca en 0.
conteo.set(heroe, (conteo.get(heroe) || 0) + 1);
}
// conteo → Map { 'Tracer' => 3, 'Mercy' => 1, 'Ana' => 1 }
// 3
console.log(conteo.get('Tracer'));Aquí el || 0 es seguro: un conteo nunca vale 0 de forma legítima —si la clave existe, su
valor es como mínimo 1—. Para un dato donde 0 sí fuera válido usarías ?? 0, como viste en
el capítulo de acceso seguro; en este caso concreto || 0 es a la vez correcto y, dentro del
playground, lo único que se ejecuta (el editor no entiende ??).
Para recorrer un Map tienes tres vistas: keys() (las claves), values() (los valores) y
entries() (pares [clave, valor]). La más usada es entries, desestructurando el par:
// se puede crear desde pares
const conteo = new Map([['Tracer', 3], ['Mercy', 1]]);
// entries() da [clave, valor]; lo desestructuramos en heroe y n.
for (const [heroe, n] of conteo.entries()) {
// Tracer: 3 / Mercy: 1
console.log(heroe + ': ' + n);
}
// Para pasarlo a array y, por ejemplo, ordenarlo: [...conteo.entries()]Map frente a objeto: cuándo cada uno#
No siempre necesitas un Map; muchas veces un objeto plano basta. Pero el Map gana cuando:
| Necesitas | Objeto plano | Map |
|---|---|---|
| Tamaño directo | Object.keys(obj).length | map.size |
| Orden de inserción garantizado | sí para claves de texto; las numéricas se reordenan | siempre |
| Claves que no sean strings | no (todo se vuelve string) | sí (cualquier tipo, incluso objetos) |
| Añadir/quitar a menudo | aceptable | pensado para ello |
| Una “forma” fija conocida (un DTO) | ideal | excesivo |
Regla práctica: si tienes un registro que crece y mengua (un conteo, una caché, un índice
por id), tira de Map. Si modelas una cosa con campos fijos (un héroe con nombre, rol,
partidas), un objeto es lo natural.
Para completar: WeakSet y WeakMap#
JavaScript también tiene WeakSet y WeakMap. Son variantes con referencias débiles: sus
entradas no impiden que el recolector de basura libere los objetos que guardan como claves. Eso
los hace útiles para asociar metadatos a objetos sin provocar fugas de memoria. No tienen .size,
no son iterables y sus claves deben ser objetos (no primitivos). Son herramientas avanzadas que
verás en librerías y código de infraestructura; por ahora basta con saber que existen y en qué se
diferencian de sus versiones normales.
Pruébalo tú#
Edita el código y pulsa Ejecutar (o Ctrl+Enter) para ver la consola. Empieza por añadir una
partida de 'Genji' al array y observa cómo cambian size y el conteo.
Comprueba lo que sabes#
Pregunta 1 de 5
¿Qué pasa al hacer new Set([1, 2, 2, 3, 3, 3])?
Tu turno#
Saca del registro los héroes únicos y cuenta las partidas de cada uno, eligiendo la estructura
adecuada, y muestra los dos resúmenes por la consola. Cuando lo tengas (o si te atascas), despliega
las soluciones y fíjate en cómo el nivel Excelente usa un único Map para el conteo, los únicos y
el ranking.
Ejercicio · en esta página
Únicos y conteos con Set y Map
A partir del registro de partidas (donde los héroes se repiten), lista los héroes únicos jugados y cuenta cuántas partidas jugó cada uno. Muestra ambos resúmenes por la consola.
Paso 1: Que funcione
- Listas los héroes únicos (sin repetir).
- Cuentas las partidas de cada héroe.
- Ambos resúmenes se ven en la consola.
Paso 2: Que esté pulido
- Únicos con Set.
- Conteo con Map (get/set con valor por defecto).
- Recorres el Map con entries() para pintar.
Paso 3: Que sea excelente
- Funciones puras y una sola pasada para el conteo.
- Los únicos salen de las claves del Map, sin recorrer dos veces.
- Derivas un ranking del mismo Map y separas cálculo de render.
Ver soluciones
// ════════════════════════════════════════════════════════════════════════════
// NIVEL OK — "que funcione"
//
// Resuelve a mano: para los únicos, recorre y va metiendo en un array solo si no
// estaba (includes); para el conteo, usa un objeto plano. Funciona, y mostrar el
// resultado correcto es el primer requisito.
//
// Sus límites (los pule Mejor): includes en cada vuelta recorre el array entero
// (cuadrático), y el objeto como contador tiene trampas (claves siempre string,
// sin .size, choca con propiedades heredadas). Set y Map existen para esto.
// ════════════════════════════════════════════════════════════════════════════
// Copia autocontenida de los datos.
const partidas = [
{ heroe: "Tracer", rol: "Daño", resultado: "victoria" },
{ heroe: "Mercy", rol: "Apoyo", resultado: "victoria" },
{ heroe: "Tracer", rol: "Daño", resultado: "derrota" },
{ heroe: "Reinhardt", rol: "Tanque", resultado: "victoria" },
{ heroe: "Mercy", rol: "Apoyo", resultado: "derrota" },
{ heroe: "Tracer", rol: "Daño", resultado: "victoria" },
{ heroe: "Ana", rol: "Apoyo", resultado: "victoria" },
{ heroe: "Reinhardt", rol: "Tanque", resultado: "derrota" },
{ heroe: "Mercy", rol: "Apoyo", resultado: "victoria" },
{ heroe: "Genji", rol: "Daño", resultado: "derrota" },
];
// ─── 1) Únicos a mano con includes ──────────────────────────────────────────
function heroesUnicos(registro) {
const unicos = [];
for (const p of registro) {
// includes recorre todo cada vez
if (!unicos.includes(p.heroe)) unicos.push(p.heroe);
}
return unicos;
}
// ─── 2) Conteo con un objeto plano ──────────────────────────────────────────
function partidasPorHeroe(registro) {
const conteo = {};
for (const p of registro) {
// si no existe, arranca en 0
conteo[p.heroe] = (conteo[p.heroe] || 0) + 1;
}
// un objeto, no un Map
return conteo;
}
// ─── 3) Mostrar ─────────────────────────────────────────────────────────────
function mostrar(unicos, conteo) {
// héroes únicos con total
console.log("Héroes únicos (" + unicos.length + "):");
for (const n of unicos) {
console.log(" " + n);
}
// partidas por héroe
console.log("Partidas por héroe:");
for (const h of Object.keys(conteo)) {
console.log(" " + h + ": " + conteo[h]);
}
}
mostrar(heroesUnicos(partidas), partidasPorHeroe(partidas)); Por qué este nivel
- Únicos a mano con includes y conteo con un objeto plano. Funciona y muestra el resultado correcto.
- Su límite: includes recorre el array entero en cada vuelta (cuadrático).
- El objeto como contador tiene trampas: claves siempre string, sin .size, y puede chocar con propiedades heredadas.
// ════════════════════════════════════════════════════════════════════════════
// NIVEL MEJOR — "que esté pulido"
//
// Por qué mejora a OK:
// - Únicos con Set: new Set(nombres) descarta duplicados solo. Comprobar si un
// valor está (Set.has) es inmediato, no recorre el array como includes.
// - Conteo con Map: get/set con un valor por defecto. Map es la estructura
// hecha para clave -> valor: tiene .size, conserva el orden de inserción y
// no se mezcla con propiedades heredadas como un objeto plano.
// - [...set] y Map.entries() convierten de vuelta a algo que se recorre fácil.
//
// Qué deja para Excelente: funciones puras, una sola pasada y derivar de ahí un
// ranking ordenado por número de partidas.
// ════════════════════════════════════════════════════════════════════════════
// Copia autocontenida de los datos.
const partidas = [
{ heroe: "Tracer", rol: "Daño", resultado: "victoria" },
{ heroe: "Mercy", rol: "Apoyo", resultado: "victoria" },
{ heroe: "Tracer", rol: "Daño", resultado: "derrota" },
{ heroe: "Reinhardt", rol: "Tanque", resultado: "victoria" },
{ heroe: "Mercy", rol: "Apoyo", resultado: "derrota" },
{ heroe: "Tracer", rol: "Daño", resultado: "victoria" },
{ heroe: "Ana", rol: "Apoyo", resultado: "victoria" },
{ heroe: "Reinhardt", rol: "Tanque", resultado: "derrota" },
{ heroe: "Mercy", rol: "Apoyo", resultado: "victoria" },
{ heroe: "Genji", rol: "Daño", resultado: "derrota" },
];
// ─── 1) Únicos con Set ──────────────────────────────────────────────────────
function heroesUnicos(registro) {
// todos los nombres, con repes
const nombres = registro.map((p) => p.heroe);
// Set quita duplicados; spread vuelve a array
return [...new Set(nombres)];
}
// ─── 2) Conteo con Map ──────────────────────────────────────────────────────
function partidasPorHeroe(registro) {
const conteo = new Map();
for (const p of registro) {
// get actual + 1
conteo.set(p.heroe, (conteo.get(p.heroe) || 0) + 1);
}
return conteo;
}
// ─── 3) Mostrar ─────────────────────────────────────────────────────────────
function mostrar(unicos, conteo) {
// héroes únicos con total
console.log("Héroes únicos (" + unicos.length + "):");
for (const n of unicos) {
console.log(" " + n);
}
// Map.entries() da pares [clave, valor]; los desestructuramos.
console.log("Partidas por héroe:");
for (const [heroe, n] of conteo.entries()) {
console.log(" " + heroe + ": " + n);
}
}
mostrar(heroesUnicos(partidas), partidasPorHeroe(partidas)); Por qué es mejor que el anterior
- Únicos con Set ([...new Set(nombres)]): descarta duplicados solo y comprobar pertenencia es inmediato.
- Conteo con Map (get/set con valor por defecto): la estructura hecha para clave -> valor, con .size y orden de inserción.
- Recorre el Map con entries() desestructurando [heroe, n].
// ════════════════════════════════════════════════════════════════════════════
// NIVEL EXCELENTE — "óptimo"
//
// Por qué mejora a Mejor:
// - Funciones puras: cada una recibe el registro y devuelve su resultado, sin
// tocar nada de fuera. Misma entrada, misma salida.
// - El conteo se construye en UNA pasada con reduce sobre un Map (acumulador),
// y de ese mismo Map sale el ranking ordenado por partidas, sin recorrer los
// datos otra vez: el Map ya tiene el resumen.
// - Los únicos salen GRATIS del Map (sus claves ya son los héroes sin repetir):
// conteo.keys(). No hace falta un segundo recorrido ni un Set aparte.
// - El mostrar solo lee datos ya calculados: cálculo y presentación separados.
//
// Idea de fondo: elige la estructura por lo que vas a hacer con los datos. Aquí
// un Map de conteo es a la vez el contador, la fuente de los únicos y la base del
// ranking. Una estructura bien elegida ahorra pasadas y código.
// ════════════════════════════════════════════════════════════════════════════
// Copia autocontenida de los datos.
const partidas = [
{ heroe: "Tracer", rol: "Daño", resultado: "victoria" },
{ heroe: "Mercy", rol: "Apoyo", resultado: "victoria" },
{ heroe: "Tracer", rol: "Daño", resultado: "derrota" },
{ heroe: "Reinhardt", rol: "Tanque", resultado: "victoria" },
{ heroe: "Mercy", rol: "Apoyo", resultado: "derrota" },
{ heroe: "Tracer", rol: "Daño", resultado: "victoria" },
{ heroe: "Ana", rol: "Apoyo", resultado: "victoria" },
{ heroe: "Reinhardt", rol: "Tanque", resultado: "derrota" },
{ heroe: "Mercy", rol: "Apoyo", resultado: "victoria" },
{ heroe: "Genji", rol: "Daño", resultado: "derrota" },
];
// ─── Resumen en una sola pasada: Map heroe -> nº de partidas ────────────────
// Pura. reduce arrastra el Map como acumulador y lo devuelve actualizado.
function contarPartidas(registro) {
return registro.reduce((conteo, p) => {
conteo.set(p.heroe, (conteo.get(p.heroe) || 0) + 1);
return conteo;
}, new Map());
}
// ─── Los únicos son las claves del Map: ya no hay duplicados ─────────────────
function unicosDe(conteo) {
return [...conteo.keys()];
}
// ─── Ranking por nº de partidas, derivado del mismo Map ─────────────────────
function rankingPorPartidas(conteo) {
// b[1]/a[1] = el conteo
return [...conteo.entries()].sort((a, b) => b[1] - a[1]);
}
// ─── Mostrar: solo lee lo ya calculado ───────────────────────────────────────
function mostrar(conteo) {
// saca los únicos y el ranking del conteo ya calculado: mostrar no recalcula nada
const unicos = unicosDe(conteo);
const ranking = rankingPorPartidas(conteo);
// héroes únicos con total
console.log(`Héroes únicos (${unicos.length}):`);
for (const n of unicos) {
console.log(" " + n);
}
// ranking por partidas, de más a menos
console.log("Ranking por partidas:");
for (const [heroe, n] of ranking) {
// singular o plural según el número de partidas
console.log(` ${heroe}: ${n} ${n === 1 ? "partida" : "partidas"}`);
}
}
mostrar(contarPartidas(partidas)); Por qué es mejor que el anterior
- Funciones puras y una sola pasada con reduce para construir el Map de conteo.
- Los únicos salen GRATIS de las claves del Map (keys()): sin segundo recorrido ni Set aparte.
- Del mismo Map deriva el ranking ordenado. Elegir bien la estructura ahorra pasadas y código.