Tienes una lista de héroes que te llega del backend y necesitas mostrarla en la interfaz:
rankeada, filtrada, agrupada, con campos calculados. Esto es el trabajo diario del
frontend. La forma torpe de hacerlo son bucles for y variables que vas acumulando a mano.
La forma profesional son los métodos de array: map, filter y reduce.
La diferencia no es estética. El código con métodos de array dice qué quieres lograr; el código con bucles describe cómo moverte por la lista paso a paso. Lo primero se lee, se mantiene y se equivoca menos.
Trabajaremos con un dataset que nos acompañará en todo el curso: héroes de un Overwatch Team Builder, cada uno con
rol,partidasyvictorias.
map: transformar cada elemento#
map recorre el array y devuelve uno nuevo del mismo tamaño, con cada elemento pasado
por tu función. El original no se toca. Es la herramienta para convertir datos del servidor
(DTO) en datos para la pantalla (ViewModel).
// map recibe una función y la aplica a cada elemento del array.
// La función devuelve un objeto nuevo: spread del héroe original ({ ...h })
// más un campo extra 'winrate' calculado al vuelo.
// El resultado es un array NUEVO del mismo tamaño; 'heroes' no cambia.
const conWinrate = heroes.map((h) => ({
// copia todos los campos del héroe original
...h,
// añade el campo winrate calculado
winrate: h.victorias / h.partidas,
}));
// conWinrate → [{ nombre: 'Tracer', rol: 'Daño', partidas: 120, victorias: 78, winrate: 0.65 }, ...]Fíjate en el { ...h, winrate }: creamos un objeto nuevo copiando el héroe y añadiendo
un campo. No mutamos el original. Esa disciplina —no pisar los datos de entrada— es lo que
hace que una app grande no se vuelva un campo de minas.
Un detalle de sintaxis fácil de pasar por alto: cuando una arrow function devuelve un objeto
directamente, hay que envolverlo en paréntesis → (h) => ({ ...h }). Sin ellos,
JavaScript lee las llaves { como el principio del cuerpo de la función (un bloque de
código), no como un objeto, y la función acaba sin devolver nada. Los paréntesis aclaran
“esto es un objeto que quiero devolver”. Es el error de sintaxis más común al empezar con map.
forEach: ejecutar algo por cada elemento#
map transforma y devuelve un array nuevo. A veces no quieres transformar nada: solo
hacer algo con cada elemento —imprimirlo, guardarlo, pintarlo en la página—. Para eso
está forEach: recorre el array y ejecuta tu función con cada elemento, pero no devuelve
nada (su resultado es undefined). Es el primo de for...of que ya conoces, en forma de método.
// forEach recorre el array y ejecuta la función una vez por cada héroe.
// No construye ningún array nuevo: solo ejecuta el cuerpo por cada elemento.
heroes.forEach((h) => {
// se imprime una línea por cada héroe de la lista
console.log(h.nombre + ' juega de ' + h.rol);
});
// forEach no devuelve nada útil: no tiene sentido guardar su resultado.Regla práctica: si vas a construir un array nuevo a partir de otro, usa map. Si solo
quieres provocar un efecto (mostrar, guardar, enviar) sin producir un valor, usa forEach.
Lo usarás sin parar en el próximo capítulo, cuando empieces a pintar elementos en la página.
filter: quedarte con algunos#
filter devuelve un array nuevo solo con los elementos para los que tu función devuelve
true. El caso típico: una búsqueda, un filtro de la interfaz.
// Recorremos todos los héroes y nos quedamos solo con los de rol 'Apoyo'.
// La condición h.rol === 'Apoyo' devuelve true o false para cada elemento.
// filter incluye en el resultado solo los que devuelven true.
const apoyos = heroes.filter((h) => h.rol === 'Apoyo');
// apoyos → [{ nombre: 'Mercy', ... }, { nombre: 'Ana', ... }]
// El array original 'heroes' no cambia.find: el primero que cumple#
filter recoge todos los que pasan la condición. Si lo que necesitas es solo el
primero, usas find. Devuelve el elemento en sí —no un array—, o undefined si
ninguno cumple.
// Buscamos el primer héroe con winrate >= 60% (victorias / partidas >= 0.60).
// find recorre el array de izquierda a derecha y para en cuanto la condición
// devuelve true. Devuelve el ELEMENTO, no un array.
const primerBueno = heroes.find((h) => h.victorias / h.partidas >= 0.60);
// primerBueno → { nombre: 'Tracer', rol: 'Daño', partidas: 120, victorias: 78 }
// Tracer está primero en el array (78/120 = 0.65 >= 0.60), así que find para ahí.
// Los demás que también cumplen (Mercy, Ana) no se devuelven: find solo da el primero.
// Si ningún héroe cumple la condición, find devuelve undefined:
// imposible → undefined (ningún héroe supera el 99% de winrate)
const imposible = heroes.find((h) => h.victorias / h.partidas > 0.99);
// → 'Primero con winrate >= 60%: Tracer'
console.log('Primero con winrate >= 60%: ' + (primerBueno ? primerBueno.nombre : 'ninguno'));findIndex: la posición, no el elemento#
find te da el elemento. Su pariente findIndex te da la posición (el índice) del primer
elemento que cumple, o -1 si no cumple ninguno. Lo necesitas cuando quieres actualizar o
quitar un elemento concreto de una lista: primero localizas su índice, luego operas sobre él.
// findIndex recorre de izquierda a derecha y devuelve el ÍNDICE del primero que cumple.
// Posición del primer héroe de rol 'Apoyo' en el array.
const indiceApoyo = heroes.findIndex((h) => h.rol === 'Apoyo');
// 2 — Mercy está en la posición 2 (recuerda: los índices empiezan en 0)
console.log(indiceApoyo);
// Si ningún elemento cumple, findIndex devuelve -1 (ojo: no undefined, como sí hacía find).
const indiceImposible = heroes.findIndex((h) => h.victorias / h.partidas > 0.99);
// -1
console.log(indiceImposible);some y every: preguntas de sí o no#
A veces no necesitas los datos en sí, sino una respuesta booleana sobre la colección.
some devuelve true si al menos un elemento cumple la condición. Para en cuanto
encuentra el primero, igual que find.
// ¿Hay algún tanque en la plantilla?
const hayTanque = heroes.some((h) => h.rol === 'Tanque');
// hayTanque → true (Reinhardt y Winston son tanques)
// ¿Hay algún héroe con winrate superior al 90%?
const hayEstrella = heroes.some((h) => h.victorias / h.partidas > 0.90);
// hayEstrella → false (ninguno supera el 90%)
// true
console.log('¿Hay algún tanque? ' + hayTanque);
// false
console.log('¿Hay algún héroe con >90% winrate? ' + hayEstrella);every devuelve true solo si todos los elementos cumplen la condición. Para en
cuanto encuentra el primero que falla.
// ¿Tienen todos los héroes al menos 50 partidas?
const todosConExperiencia = heroes.every((h) => h.partidas >= 50);
// todosConExperiencia → true (el mínimo es Winston con 80)
// ¿Tienen todos un winrate superior al 50%?
const todosBuenos = heroes.every((h) => h.victorias / h.partidas > 0.50);
// todosBuenos → false (Winston tiene 38/80 = 0.475, que no supera el 50%)
// true
console.log('¿Todos con >= 50 partidas? ' + todosConExperiencia);
// false
console.log('¿Todos con winrate > 50%? ' + todosBuenos);La diferencia clave entre los tres:
| Método | ¿Qué devuelve? | ¿Cuándo para? |
|---|---|---|
filter | array con todos los que cumplen | recorre siempre todo |
find | el primer elemento que cumple (o undefined) | al encontrar el primero |
some | true / false | al encontrar el primero que cumple |
every | true / false | al encontrar el primero que falla |
reduce: condensar en un solo valor#
reduce es el más potente y el que más asusta. Arrastra un acumulador a lo largo de la
lista y devuelve un único resultado: una suma, un máximo, un objeto agrupado… lo que
quieras construir.
// Primero, con map calculamos el winrate de cada héroe como número entre 0 y 1.
// El resultado es un array de números: [0.65, 0.566..., 0.65, 0.48, 0.6, 0.475].
// Luego, reduce los suma todos arrastrando un acumulador que empieza en 0.
// Por último, dividimos entre el total de héroes para obtener la media.
const medio = heroes
// → array de winrates individuales
.map((h) => h.victorias / h.partidas)
// → suma total de todos los winrates
.reduce((suma, w) => suma + w, 0)
// → media (suma / número de héroes)
/ heroes.length;
// medio ≈ 0.570 → (0.570 * 100).toFixed(1) → "57.0%"Su segundo caso estrella es agrupar. Pasar de una lista plana a un objeto
{ Tanque: [...], Daño: [...], Apoyo: [...] } es, otra vez, un reduce:
// Arrancamos con un acumulador vacío: un objeto sin propiedades {}.
// Por cada héroe, comprobamos si ya existe un array para su rol.
// Si no existe (undefined → falsy), lo creamos con [].
// Luego metemos al héroe en ese array y devolvemos el acumulador actualizado.
const porRol = heroes.reduce((grupos, h) => {
// crea el array del rol si aún no existe
grupos[h.rol] = grupos[h.rol] || [];
// añade el héroe a su rol
grupos[h.rol].push(h);
// devuelve el acumulador para la siguiente iteración
return grupos;
// el acumulador empieza como objeto vacío
}, {});
// porRol → { Daño: [...], Tanque: [...], Apoyo: [...] }Encadenar: el patrón que verás en cualquier código real#
Lo normal no es usar un método suelto, sino encadenarlos: filtra, transforma, ordena. Como cada uno devuelve un array nuevo, se leen de arriba abajo como una receta.
Antes del ejemplo, una aclaración sobre cómo funciona .sort() con una función comparadora,
porque si no, (a, b) => b.winrate - a.winrate parece magia. El motor llama a tu función
pasándole dos elementos del array, a y b, y espera un número de vuelta: si devuelve un
número negativo, a va antes que b; si devuelve uno positivo, b va antes; si
devuelve cero, se quedan igual. Por eso b.winrate - a.winrate ordena de mayor a menor
(descendente): cuando b tiene más winrate que a, la resta da positivo y b sube. La
variante a.winrate - b.winrate haría exactamente lo contrario, ascendente:
// Comparadora ascendente: a - b.
// Si a.winrate < b.winrate → resultado negativo → a va primero (el más bajo arriba).
const rankingAscendente = [...heroes].sort((a, b) => a.victorias / a.partidas - b.victorias / b.partidas);
// rankingAscendente → el héroe con menos winrate primeroconst rankingDano = heroes
// 1) quédate solo con los héroes de Daño
.filter((h) => h.rol === 'Daño')
// 2) transforma cada uno: solo nombre y winrate
.map((h) => ({
nombre: h.nombre,
winrate: h.victorias / h.partidas,
}))
// 3) b - a → negativo cuando b > a → b sube: descendente
.sort((a, b) => b.winrate - a.winrate);
// rankingDano → [{ nombre: 'Tracer', winrate: 0.65 }, { nombre: 'Genji', winrate: 0.48 }, ...]Un aviso importante: sort sí muta el array sobre el que actúa —lo reordena en su
sitio— y encima lo devuelve. Es el único de estos métodos que no es seguro. Por eso, cuando
partes de un array que no quieres estropear, ordenas sobre una copia:
// Mal: sort muta 'heroes' directamente. Después de esto, heroes ya no está en su orden original.
heroes.sort((a, b) => b.victorias - a.victorias);
// Bien: spread crea un array nuevo. Sort reordena la copia; 'heroes' queda intacto.
const rankingSeguro = [...heroes].sort((a, b) => b.victorias - a.victorias);
// heroes → sigue en el orden original
// rankingSeguro → copia ordenada por victorias descendenteOrdenar textos: localeCompare#
El comparador de resta (a - b) solo sirve para números. Con strings da NaN y el orden
sale roto. Para ordenar alfabéticamente —nombres de héroe, por ejemplo— usas el método
.localeCompare, que compara dos strings y devuelve justo lo que sort espera: negativo,
positivo o cero.
// .localeCompare compara el string a con el b respetando el idioma (acentos, ñ...).
// Devuelve negativo, positivo o cero: el mismo contrato que pide sort.
const porNombre = [...heroes].sort((a, b) => a.nombre.localeCompare(b.nombre));
// Ana, Genji, Mercy, Reinhardt, Tracer, Winston (orden alfabético)
console.log(porNombre.map((h) => h.nombre).join(', '));Pruébalo tú#
Edita el código y pulsa Ejecutar (o Ctrl+Enter) para ver la consola. Empieza por
cambiar el filtro de 'Daño' a 'Tanque'.
Comprueba lo que sabes#
Pregunta 1 de 5
¿Qué devuelve heroes.map(fn)?
Tu turno#
Lo que se entiende leyendo no es lo mismo que lo que sabes escribir. Resuélvelo aquí mismo:
edita solucion.js, rankea y agrupa a los héroes y muéstralos por la consola. Cuando lo tengas
(o si te atascas), despliega las soluciones y fíjate en el salto de un nivel al siguiente.
Ejercicio · en esta página
Rankea y agrupa a los héroes
A partir de la lista de héroes, calcula el winrate de cada uno, devuélvelos rankeados por winrate de mayor a menor, y agrúpalos por rol. Muestra el resultado por la consola.
Paso 1: Que funcione
- El ranking sale ordenado por winrate, de mayor a menor.
- Los héroes aparecen agrupados por su rol.
- Se ve el resultado en la consola (vale resolverlo con bucles).
Paso 2: Que esté pulido
- Usas map/filter/sort/reduce en lugar de bucles manuales.
- No mutas el array original.
- Manejas la lista vacía sin que reviente.
- El winrate se muestra formateado como porcentaje.
Paso 3: Que sea excelente
- El winrate se calcula una sola vez por héroe.
- Funciones puras y reutilizables, con inmutabilidad total.
- Agrupas en una sola pasada y separas el cálculo de la presentación.
Ver soluciones
// ════════════════════════════════════════════════════════════════════════════
// NIVEL OK — "funciona"
//
// Enfoque base: bucles for manuales y acumuladores. Es la traducción directa
// de "haz esto, luego esto otro" a código. Funciona y produce el resultado
// correcto, que es el primer requisito de cualquier solución.
//
// Sus límites (los que arregla el nivel "Mejor"):
// - Mucho código repetitivo y muchas variables intermedias que vigilar.
// - El winrate se recalcula varias veces (en el ranking y en el render).
// - No formatea el winrate: lo muestra como 0.65 en vez de 65.0%.
// - El ordenamiento es un bubble sort a mano, lento y fácil de equivocar.
// Aun así: se ve en la consola y los datos son correctos. Eso vale como OK.
// ════════════════════════════════════════════════════════════════════════════
// Copia autocontenida de los datos para que esta solución se ejecute suelta.
const heroes = [
{ nombre: "Tracer", rol: "Daño", partidas: 120, victorias: 78 },
{ nombre: "Reinhardt", rol: "Tanque", partidas: 90, victorias: 51 },
{ nombre: "Mercy", rol: "Apoyo", partidas: 200, victorias: 130 },
{ nombre: "Genji", rol: "Daño", partidas: 150, victorias: 72 },
{ nombre: "Ana", rol: "Apoyo", partidas: 110, victorias: 66 },
{ nombre: "Winston", rol: "Tanque", partidas: 80, victorias: 38 },
{ nombre: "Sojourn", rol: "Daño", partidas: 60, victorias: 30 },
{ nombre: "Brigitte", rol: "Apoyo", partidas: 95, victorias: 52 },
];
// ─── 1) Ranking por winrate ────────────────────────────────────────────────
// Copiamos el array a mano para no estropear el original, y lo ordenamos con
// un bubble sort. Dentro de la comparación recalculamos el winrate cada vez:
// funciona, pero es trabajo de más.
function rankPorWinrate(lista) {
// Copia manual elemento a elemento.
const copia = [];
for (let i = 0; i < lista.length; i++) {
copia.push(lista[i]);
}
// Bubble sort descendente por winrate.
for (let i = 0; i < copia.length; i++) {
for (let j = 0; j < copia.length - 1 - i; j++) {
const winA = copia[j].victorias / copia[j].partidas;
const winB = copia[j + 1].victorias / copia[j + 1].partidas;
if (winA < winB) {
const tmp = copia[j];
copia[j] = copia[j + 1];
copia[j + 1] = tmp;
}
}
}
return copia;
}
// ─── 2) Agrupar por rol ────────────────────────────────────────────────────
// Recorremos la lista una vez y vamos metiendo cada héroe en el cajón de su
// rol. Preparamos los cajones a mano.
function agruparPorRol(lista) {
const grupos = { Tanque: [], Daño: [], Apoyo: [] };
for (let i = 0; i < lista.length; i++) {
const heroe = lista[i];
grupos[heroe.rol].push(heroe);
}
return grupos;
}
// ─── 3) Mostrar ────────────────────────────────────────────────────────────
// Imprimimos por la consola con bucles a mano. Volvemos a calcular el winrate
// aquí (tercera vez que lo calculamos en total).
function mostrar(ranking, grupos) {
console.log("Ranking por winrate:");
for (let i = 0; i < ranking.length; i++) {
const h = ranking[i];
const winrate = h.victorias / h.partidas;
console.log(i + 1 + ". " + h.nombre + " (" + h.rol + ") — " + winrate);
}
console.log("Por rol:");
const roles = ["Tanque", "Daño", "Apoyo"];
for (let i = 0; i < roles.length; i++) {
const rol = roles[i];
const delRol = grupos[rol];
// construimos la lista de nombres a mano, separando con comas
let nombres = "";
for (let j = 0; j < delRol.length; j++) {
nombres += delRol[j].nombre;
if (j < delRol.length - 1) nombres += ", ";
}
console.log(rol + ": " + nombres);
}
}
// ─── Arranque ──────────────────────────────────────────────────────────────
const ranking = rankPorWinrate(heroes);
const grupos = agruparPorRol(heroes);
mostrar(ranking, grupos); Por qué este nivel
- Resuelve el problema con bucles for y acumuladores: la traducción literal del enunciado a código.
- Funciona y muestra datos correctos, que es el primer requisito de cualquier solución.
- Sus límites: mucho código repetido, el winrate se recalcula tres veces y el orden es un bubble sort hecho a mano.
// ════════════════════════════════════════════════════════════════════════════
// NIVEL MEJOR — "pulido"
//
// Por qué mejora a OK:
// - Cambia los bucles for manuales por métodos de array (map, sort, reduce).
// El código dice QUÉ queremos, no el mecanismo paso a paso.
// - No muta el array de entrada: sort trabaja sobre una copia ([...lista]).
// (El bubble sort de OK ya copiaba, pero ahora es explícito y de una línea.)
// - Nombres claros: heroesConWinrate, ranking, grupos.
// - Maneja la lista vacía: map/reduce sobre [] devuelven [] / {} sin romper.
// - Formatea el winrate a porcentaje legible (65.0%) en vez de 0.65.
//
// Lo que todavía deja para "Excelente":
// - El winrate se calcula al construir heroesConWinrate, pero la agrupación
// vuelve a recorrer datos en paralelo al ranking en lugar de compartir el
// cálculo. Y el formateo a % vive mezclado con el mostrado.
// ════════════════════════════════════════════════════════════════════════════
// Copia autocontenida de los datos para ejecutar esta solución suelta.
const heroes = [
{ nombre: "Tracer", rol: "Daño", partidas: 120, victorias: 78 },
{ nombre: "Reinhardt", rol: "Tanque", partidas: 90, victorias: 51 },
{ nombre: "Mercy", rol: "Apoyo", partidas: 200, victorias: 130 },
{ nombre: "Genji", rol: "Daño", partidas: 150, victorias: 72 },
{ nombre: "Ana", rol: "Apoyo", partidas: 110, victorias: 66 },
{ nombre: "Winston", rol: "Tanque", partidas: 80, victorias: 38 },
{ nombre: "Sojourn", rol: "Daño", partidas: 60, victorias: 30 },
{ nombre: "Brigitte", rol: "Apoyo", partidas: 95, victorias: 52 },
];
// Winrate como número entre 0 y 1. Protege la división por cero (partidas 0).
function winrateDe(heroe) {
return heroe.partidas === 0 ? 0 : heroe.victorias / heroe.partidas;
}
// Formatea un winrate (0..1) a porcentaje con un decimal: 0.65 → "65.0%".
function formatearWinrate(winrate) {
return (winrate * 100).toFixed(1) + "%";
}
// ─── 1) Ranking por winrate ────────────────────────────────────────────────
// Copiamos con spread para no mutar la entrada, y ordenamos descendente.
function rankPorWinrate(lista) {
return [...lista].sort((a, b) => winrateDe(b) - winrateDe(a));
}
// ─── 2) Agrupar por rol ────────────────────────────────────────────────────
// reduce: arrancamos con un acumulador vacío y vamos creando el cajón de cada
// rol la primera vez que aparece. Funciona igual con la lista vacía → {}.
function agruparPorRol(lista) {
return lista.reduce((grupos, heroe) => {
if (!grupos[heroe.rol]) grupos[heroe.rol] = [];
grupos[heroe.rol].push(heroe);
return grupos;
}, {});
}
// ─── 3) Mostrar ────────────────────────────────────────────────────────────
function mostrar(ranking, grupos) {
console.log("Ranking por winrate:");
// lista vacía: un único aviso en vez de no imprimir nada
if (ranking.length === 0) console.log("Sin héroes.");
ranking.forEach((h, i) => {
console.log(
`${i + 1}. ${h.nombre} (${h.rol}) — ${formatearWinrate(winrateDe(h))}`,
);
});
console.log("Por rol:");
Object.entries(grupos).forEach(([rol, heroesDelRol]) => {
// unimos los nombres del rol en una sola línea
const nombres = heroesDelRol.map((h) => h.nombre).join(", ");
console.log(`${rol}: ${nombres}`);
});
}
// ─── Arranque ──────────────────────────────────────────────────────────────
const ranking = rankPorWinrate(heroes);
const grupos = agruparPorRol(heroes);
mostrar(ranking, grupos); Por qué es mejor que el anterior
- Cambia los bucles por map/sort/reduce: el código dice QUÉ quieres, no el mecanismo paso a paso.
- No muta la entrada (ordena sobre una copia con [...lista]) y maneja la lista vacía sin romper.
- Formatea el winrate a porcentaje legible (65.0%) en lugar de 0.65.
- Todavía calcula el winrate en varios sitios: eso lo pule el nivel Excelente.
// ════════════════════════════════════════════════════════════════════════════
// NIVEL EXCELENTE — "óptimo"
//
// Por qué mejora a Mejor:
// - El winrate se calcula UNA sola vez por héroe. Enriquecemos cada héroe con
// su winrate al principio (con winrateDe) y a partir de ahí nadie vuelve a
// dividir: ni el sort, ni la agrupación, ni el render lo recalculan.
// - Inmutabilidad total: no mutamos los héroes de entrada; creamos objetos
// nuevos con spread ({ ...heroe, winrate }). La entrada queda intacta.
// - Funciones puras y reutilizables: cada una depende solo de sus argumentos
// y devuelve siempre lo mismo para la misma entrada. Sin estado oculto.
// - Agrupación en una sola pasada con reduce (no recorremos la lista una vez
// por rol). El formateo a % vive aislado en formatearWinrate.
// - Complejidad comentada en cada paso.
//
// Coste global: O(n log n), dominado por el sort. El enriquecido y la
// agrupación son O(n) cada uno, así que no cambian el orden de magnitud.
// ════════════════════════════════════════════════════════════════════════════
// Copia autocontenida de los datos para ejecutar esta solución suelta.
const heroes = [
{ nombre: "Tracer", rol: "Daño", partidas: 120, victorias: 78 },
{ nombre: "Reinhardt", rol: "Tanque", partidas: 90, victorias: 51 },
{ nombre: "Mercy", rol: "Apoyo", partidas: 200, victorias: 130 },
{ nombre: "Genji", rol: "Daño", partidas: 150, victorias: 72 },
{ nombre: "Ana", rol: "Apoyo", partidas: 110, victorias: 66 },
{ nombre: "Winston", rol: "Tanque", partidas: 80, victorias: 38 },
{ nombre: "Sojourn", rol: "Daño", partidas: 60, victorias: 30 },
{ nombre: "Brigitte", rol: "Apoyo", partidas: 95, victorias: 52 },
];
// ─── Funciones puras y reutilizables ───────────────────────────────────────
// Winrate como número entre 0 y 1. Pura. O(1). Protege la división por cero.
function winrateDe(heroe) {
return heroe.partidas === 0 ? 0 : heroe.victorias / heroe.partidas;
}
// Formatea un winrate (0..1) a porcentaje con un decimal. Pura. O(1).
function formatearWinrate(winrate) {
return (winrate * 100).toFixed(1) + "%";
}
// Enriquece la lista: cada héroe pasa a llevar su winrate ya calculado.
// Crea objetos NUEVOS (no muta la entrada). Pura. O(n). El winrate se calcula
// aquí una única vez; de aquí en adelante nadie vuelve a dividir.
function conWinrate(lista) {
return lista.map((heroe) => ({ ...heroe, winrate: winrateDe(heroe) }));
}
// ─── 1) Ranking por winrate ────────────────────────────────────────────────
// Recibe héroes YA enriquecidos: el comparador solo lee el campo winrate, no
// recalcula nada. Copia con spread para no mutar. O(n log n) por el sort.
function rankPorWinrate(heroesConWinrate) {
return [...heroesConWinrate].sort((a, b) => b.winrate - a.winrate);
}
// ─── 2) Agrupar por rol ────────────────────────────────────────────────────
// Una sola pasada con reduce. Cada rol crea su array la primera vez que se ve,
// y al copiarlo con spread mantenemos la inmutabilidad del acumulador previo.
// Pura. O(n).
function agruparPorRol(heroesConWinrate) {
return heroesConWinrate.reduce((grupos, heroe) => {
// si el rol aún no tiene array, arranca con uno vacío
const previos = grupos[heroe.rol] || [];
return { ...grupos, [heroe.rol]: [...previos, heroe] };
}, {});
}
// ─── 3) Mostrar ────────────────────────────────────────────────────────────
// El mostrado solo LEE: usa el winrate ya presente en cada héroe y lo formatea.
// Ningún cálculo de winrate aquí. O(n).
function mostrar(ranking, grupos) {
console.log("Ranking por winrate:");
// lista vacía: un único aviso en vez de no imprimir nada
if (ranking.length === 0) console.log("Sin héroes.");
ranking.forEach((h, i) => {
console.log(
`${i + 1}. ${h.nombre} (${h.rol}) — ${formatearWinrate(h.winrate)}`,
);
});
console.log("Por rol:");
Object.entries(grupos).forEach(([rol, heroesDelRol]) => {
// cada nombre lleva su winrate ya calculado (el mostrado no recalcula nada)
const nombres = heroesDelRol
.map((h) => `${h.nombre} (${formatearWinrate(h.winrate)})`)
.join(", ");
console.log(`${rol}: ${nombres}`);
});
}
// ─── Arranque ──────────────────────────────────────────────────────────────
// Enriquecemos una vez y reutilizamos ese resultado para ranking y agrupación.
const enriquecidos = conWinrate(heroes);
const ranking = rankPorWinrate(enriquecidos);
const grupos = agruparPorRol(enriquecidos);
mostrar(ranking, grupos); Por qué es mejor que el anterior
- Calcula el winrate UNA sola vez por héroe (enriquece la lista) y a partir de ahí nadie vuelve a dividir.
- Inmutabilidad total y funciones puras: misma entrada, misma salida, sin estado oculto.
- Agrupa en una sola pasada con reduce; el coste global es O(n log n), dominado por el sort.
- El mostrado solo lee datos ya calculados: separa el cálculo de la presentación.