learning-front

Nivel 3 · JavaScript moderno y asíncrono

Métodos modernos de array y objeto

El segundo cinturón de métodos (ES2019-2024): transformar objetos con entries y fromEntries, aplanar con flat y flatMap, acceder con at y agrupar con groupBy.

Ya tienes los tres métodos que sostienen casi todo el trabajo con listas: map, filter y reduce. Pero el lenguaje ha seguido creciendo, y en los últimos años ha añadido un segundo cinturón de métodos que resuelven en una línea cosas que antes pedían un bucle: transformar un objeto, aplanar listas anidadas, coger el último elemento o agrupar por una clave. No son imprescindibles —todo se puede hacer a mano—, pero aparecen en código real constantemente y leerlos te ahorra trabajo.

Seguimos con el Overwatch Team Builder. Cada héroe tiene un id, un rol y una lista de mapas. Ese rol (para agrupar), ese id (para indexar) y esa lista de listas (para aplanar) son justo lo que estrenan estos métodos.

Recorrer un objeto: keys, values, entries#

Un objeto no se recorre con map como un array. Para tratarlo como una colección, lo conviertes primero en una lista con uno de estos tres métodos del objeto global Object:

javascript
const heroe = { nombre: "Tracer", rol: "Daño", partidas: 120 };

// ['nombre', 'rol', 'partidas']  (las claves)
console.log(Object.keys(heroe));
// ['Tracer', 'Daño', 120]      (los valores)
console.log(Object.values(heroe));
// [['nombre','Tracer'], ['rol','Daño'], ['partidas',120]]
console.log(Object.entries(heroe));

entries es el más potente: te da pares [clave, valor], que sueles desestructurar al recorrer (igual que hacías con Map.entries en el capítulo de Sets y Maps):

javascript
const heroe = { nombre: "Tracer", rol: "Daño" };

// Desestructuramos cada par en clave y valor.
for (const [clave, valor] of Object.entries(heroe)) {
  // nombre = Tracer / rol = Daño
  console.log(clave + " = " + valor);
}

Object.fromEntries: transformar un objeto#

fromEntries hace el camino inverso de entries: toma una lista de pares [clave, valor] y construye un objeto. Por separado parece poco útil; su valor está en combinarlo con entries y map para transformar un objeto entero (algo que no puedes hacer con map directamente, porque un objeto no tiene map).

javascript
const stats = { partidas: 120, victorias: 78 };

// Patrón estándar: entries (a pares) → map (transforma) → fromEntries (a objeto).
const dobles = Object.fromEntries(
  Object.entries(stats).map(([clave, valor]) => [clave, valor * 2]),
);
// { partidas: 240, victorias: 156 }
console.log(dobles);

Léelo de dentro afuera: entries parte el objeto en pares, map transforma cada par, fromEntries vuelve a montar el objeto. Es la forma declarativa de “mapear” un objeto.

Object.assign: fusionar objetos#

Object.assign(destino, ...fuentes) copia las propiedades de las fuentes sobre el destino, y en las claves repetidas gana la última. Es el pariente directo del spread que viste en su capítulo:

javascript
const base = { region: "Europa", modo: "Competitivo" };

// Copia base y luego { modo: 'Amistoso' } sobre un objeto nuevo ({}).
const fusion = Object.assign({}, base, { modo: "Amistoso" });
// { region: 'Europa', modo: 'Amistoso' }
console.log(fusion);
// Equivale a: const fusion = { ...base, modo: 'Amistoso' };

En código nuevo se prefiere el spread ({ ...base, modo: "Amistoso" }) por ser más legible, pero Object.assign aparece en mucho código existente: conviene reconocerlo.

Aplanar: flat y flatMap#

Cuando tienes un array de arrays, flat lo aplana un nivel y flatMap hace map y aplana en una sola pasada.

javascript
const anidado = [
  ["Hanamura", "Dorado"],
  ["Ilios"],
  ["Eichenwalde", "Nepal"],
];

// flat: junta las sub-listas en una sola (un nivel).
// ['Hanamura', 'Dorado', 'Ilios', 'Eichenwalde', 'Nepal']
console.log(anidado.flat());

flatMap es el atajo de “mapea y aplana”, muy útil cuando cada elemento produce una lista:

javascript
const roster = [
  { nombre: "Tracer", mapas: ["Hanamura", "Dorado"] },
  { nombre: "Mercy", mapas: ["Ilios"] },
];

// map daría [['Hanamura','Dorado'], ['Ilios']]; flatMap lo aplana de una vez.
const mapas = roster.flatMap((h) => h.mapas);
// ['Hanamura', 'Dorado', 'Ilios']
console.log(mapas);

Acceder por posición: at#

at(i) accede al elemento en la posición i, igual que los corchetes. Su gracia es que acepta índices negativos que cuentan desde el final:

javascript
const ranking = ["Mercy", "Ana", "Tracer"];

// Mercy   (igual que ranking[0])
console.log(ranking.at(0));
// Tracer (el último, sin ranking[ranking.length - 1])
console.log(ranking.at(-1));
// Ana    (el penúltimo)
console.log(ranking.at(-2));

Es solo más legible, pero ese at(-1) para “el último” se lee mucho mejor que ranking[ranking.length - 1].

findLast: buscar desde el final#

Ya conoces find, que devuelve el primer elemento que cumple una condición. findLast hace lo mismo pero buscando desde el final: devuelve el último que cumple.

javascript
const roster = [
  { nombre: "Tracer", rol: "Daño" },
  { nombre: "Mercy", rol: "Apoyo" },
  { nombre: "Ana", rol: "Apoyo" },
];

// find daría Mercy (el primer Apoyo); findLast da Ana (el último Apoyo).
// Ana
console.log(roster.findLast((h) => h.rol === "Apoyo").nombre);

Object.groupBy: agrupar por una clave#

El más reciente y, para datos, el más útil. Object.groupBy(lista, fn) recorre la lista y agrupa cada elemento por el valor que devuelve fn. El resultado es un objeto: una clave por grupo, y un array de elementos en cada una.

javascript
const roster = [
  { nombre: "Tracer", rol: "Daño" },
  { nombre: "Mercy", rol: "Apoyo" },
  { nombre: "Reinhardt", rol: "Tanque" },
  { nombre: "Ana", rol: "Apoyo" },
];

// Agrupa por rol: una clave por rol, con su array de héroes.
const porRol = Object.groupBy(roster, (h) => h.rol);
console.log(porRol);
// { Daño: [Tracer], Apoyo: [Mercy, Ana], Tanque: [Reinhardt] }
// 2
console.log(porRol["Apoyo"].length);

Es un método reciente (ES2024), ya disponible en los navegadores modernos y en Node 21+. Si un proyecto tiene que soportar entornos antiguos, compruébalo (en caniuse) o resuélvelo con reduce a mano, como se hacía antes. Para tableros, rankings y resúmenes por categoría, es de los que más vas a usar.

Nota: tanto Object.groupBy como su variante Map.groupBy (que devuelve un Map en lugar de un objeto plano) requieren un navegador publicado en 2024 o posterior. En proyectos que deban funcionar en entornos más antiguos, sustitúyelos por un reduce clásico hasta que puedas asumir ese soporte.

Pruébalo tú#

Edita el código y pulsa Ejecutar (o Ctrl+Enter) para ver la consola. Empieza por la última línea: usa Object.groupBy para agrupar el roster por su número de mapas (h.mapas.length).

Comprueba lo que sabes#

Pregunta 1 de 5

¿Qué devuelve Object.fromEntries([['nombre', 'Tracer'], ['rol', 'Daño']])?

Tu turno#

Leerlo no es lo mismo que saber escribirlo. Resuélvelo aquí: agrupa el roster por rol, indéxalo por id y reúne todos los mapas en una lista. Cuando lo tengas (o si te atascas), despliega las soluciones y fíjate en cómo el nivel Excelente encadena los métodos en una tubería.

Ejercicio · en esta página

Resúmenes del roster con métodos modernos

A partir del roster (héroes con id, rol y una lista de mapas), agrupa por rol, indexa por id para buscar rápido y reúne todos los mapas en una sola lista. Muestra los tres resultados por la consola.

Paso 1: Que funcione

  • Agrupas por rol, indexas por id y reúnes todos los mapas.
  • Los tres resultados se ven en la consola (vale a mano con bucles).
Ver soluciones
// ════════════════════════════════════════════════════════════════════════════
// NIVEL OK — "funciona"
//
// Resuelve todo a mano con bucles: agrupa creando arrays sobre la marcha, indexa
// rellenando un objeto y aplana con dos bucles anidados. Funciona y da el
// resultado correcto.
//
// Su límite: es mucho código mecánico para tres operaciones que el lenguaje ya
// resuelve en una línea cada una. Más sitio donde equivocarse y más que leer.
// ════════════════════════════════════════════════════════════════════════════

const roster = [
  {
    id: "h1",
    nombre: "Tracer",
    rol: "Daño",
    partidas: 120,
    victorias: 78,
    mapas: ["Hanamura", "Dorado"],
  },
  {
    id: "h2",
    nombre: "Mercy",
    rol: "Apoyo",
    partidas: 200,
    victorias: 130,
    mapas: ["Ilios"],
  },
  {
    id: "h3",
    nombre: "Reinhardt",
    rol: "Tanque",
    partidas: 90,
    victorias: 51,
    mapas: ["Eichenwalde", "Nepal"],
  },
  {
    id: "h4",
    nombre: "Ana",
    rol: "Apoyo",
    partidas: 140,
    victorias: 88,
    mapas: ["Ilios", "Dorado"],
  },
  {
    id: "h5",
    nombre: "Genji",
    rol: "Daño",
    partidas: 150,
    victorias: 72,
    mapas: ["Hanamura"],
  },
];

// Agrupar a mano: por cada héroe, crea el array de su rol si no existe y mete dentro.
function agruparPorRol(heroes) {
  const grupos = {};
  for (const h of heroes) {
    // primera vez que aparece ese rol
    if (!grupos[h.rol]) grupos[h.rol] = [];
    grupos[h.rol].push(h);
  }
  return grupos;
}

// Indexar a mano: un objeto id -> héroe, rellenado en un bucle.
function indexarPorId(heroes) {
  const indice = {};
  for (const h of heroes) {
    indice[h.id] = h;
  }
  return indice;
}

// Aplanar a mano: dos bucles, uno dentro de otro, para vaciar cada sub-lista.
function todosLosMapas(heroes) {
  const mapas = [];
  for (const h of heroes) {
    for (const m of h.mapas) {
      mapas.push(m);
    }
  }
  return mapas;
}

function mostrar() {
  const porRol = agruparPorRol(roster);
  const indice = indexarPorId(roster);
  const mapas = todosLosMapas(roster);

  const rolesTexto = Object.keys(porRol)
    .map((rol) => rol + ": " + porRol[rol].length)
    .join(" · ");

  // imprime los resultados de las tres operaciones por consola
  console.log("Héroes por rol: " + rolesTexto);
  console.log("Buscar por id (h3): " + indice["h3"].nombre);
  console.log("Todos los mapas (" + mapas.length + "): " + mapas.join(", "));
}

mostrar();

Por qué este nivel

  • Resuelve a mano con bucles: agrupa creando arrays sobre la marcha, indexa rellenando un objeto y aplana con bucles anidados. Funciona y da el resultado correcto.
  • Es mucho código mecánico para tres operaciones que el lenguaje resuelve en una línea cada una.
  • Más sitio donde equivocarse y más que leer.