learning-front

Nivel 2 · JavaScript: fundamentos del lenguaje

Transformar datos con array methods

map, filter y reduce: la forma declarativa de trabajar con colecciones de datos, con ejemplos reales en vez de bucles de juguete.

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, partidas y victorias.

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

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

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

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

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

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

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

javascript
// ¿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?
filterarray con todos los que cumplenrecorre siempre todo
findel primer elemento que cumple (o undefined)al encontrar el primero
sometrue / falseal encontrar el primero que cumple
everytrue / falseal 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.

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

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

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

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

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

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