learning-front

Nivel 3 · JavaScript moderno y asíncrono

Sets y Maps

Dos colecciones que ganan al array y al objeto cuando necesitas valores únicos (Set) o un diccionario clave-valor de verdad (Map), con su iteración y su tamaño.

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.

javascript
// 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:

javascript
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:

javascript
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.

javascript
// 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:

javascript
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:

javascript
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:

javascript
// 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:

NecesitasObjeto planoMap
Tamaño directoObject.keys(obj).lengthmap.size
Orden de inserción garantizadosí para claves de texto; las numéricas se reordenansiempre
Claves que no sean stringsno (todo se vuelve string)sí (cualquier tipo, incluso objetos)
Añadir/quitar a menudoaceptablepensado para ello
Una “forma” fija conocida (un DTO)idealexcesivo

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.
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.