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, unroly una lista demapas. Eserol(para agrupar), eseid(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:
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):
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).
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:
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.
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:
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:
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.
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.
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.groupBycomo su varianteMap.groupBy(que devuelve unMapen 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 unreduceclá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).
Paso 2: Que esté pulido
- Agrupas con Object.groupBy.
- Indexas con Object.fromEntries.
- Aplanas los mapas con flatMap.
Paso 3: Que sea excelente
- Encadenas entries → map → fromEntries para derivar un resumen del agrupado.
- Quitas los mapas duplicados con un Set.
- Funciones puras y render separado del cálculo.
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.
// ════════════════════════════════════════════════════════════════════════════
// NIVEL MEJOR — "pulido"
//
// Por qué mejora a OK:
// - Agrupar: Object.groupBy(heroes, h => h.rol) en una línea, sin crear arrays
// a mano ni el if de "primera vez".
// - Indexar: Object.fromEntries(heroes.map(h => [h.id, h])) — de pares a objeto.
// - Aplanar: heroes.flatMap(h => h.mapas) — map + aplanar un nivel, de una pasada.
// Cada operación es declarativa: dice QUÉ quieres, no el bucle de CÓMO.
// ════════════════════════════════════════════════════════════════════════════
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"],
},
];
// Agrupa la lista por la clave que devuelve la función: { Daño: [...], Apoyo: [...] }.
function agruparPorRol(heroes) {
return Object.groupBy(heroes, (h) => h.rol);
}
// Convierte la lista en pares [id, héroe] y de ahí a un objeto id -> héroe.
function indexarPorId(heroes) {
return Object.fromEntries(heroes.map((h) => [h.id, h]));
}
// flatMap saca el array `mapas` de cada héroe y los aplana en una sola lista.
function todosLosMapas(heroes) {
return heroes.flatMap((h) => h.mapas);
}
function mostrar() {
const porRol = agruparPorRol(roster);
const indice = indexarPorId(roster);
const mapas = todosLosMapas(roster);
const rolesTexto = Object.entries(porRol)
.map(([rol, hs]) => `${rol}: ${hs.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é es mejor que el anterior
- Object.groupBy para agrupar, Object.fromEntries para indexar y flatMap para aplanar: cada operación en una línea declarativa.
- Dice QUÉ quieres, no el bucle de CÓMO.
- Su límite respecto a Excelente: no compone las operaciones ni quita duplicados.
// ════════════════════════════════════════════════════════════════════════════
// NIVEL EXCELENTE — "óptimo"
//
// Por qué mejora a Mejor:
// - resumenPorRol encadena agrupar + transformar el objeto: Object.entries del
// agrupado → map a [rol, nº] → Object.fromEntries. Es el pipeline estándar para
// "transformar un objeto" (entries → map → fromEntries) y deja un resumen limpio.
// - mapasUnicos combina flatMap con un Set (capítulo de Sets y Maps): aplana y
// quita duplicados en una expresión.
// - Funciones puras (entran datos, salen datos) y un mostrar que solo presenta.
//
// Idea de fondo: estos métodos componen. Encadenarlos describe la transformación
// como una tubería legible, en vez de bucles que hay que leer paso a paso.
// ════════════════════════════════════════════════════════════════════════════
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"],
},
];
// Agrupa por rol y transforma cada grupo en su nº de héroes: { Daño: 2, Apoyo: 2, ... }.
function resumenPorRol(heroes) {
// { rol: [héroes] }
const grupos = Object.groupBy(heroes, (h) => h.rol);
return Object.fromEntries(
// [rol, nº]
Object.entries(grupos).map(([rol, hs]) => [rol, hs.length]),
);
}
// Índice id -> héroe, para buscar en O(1) sin recorrer la lista.
function indicePorId(heroes) {
return Object.fromEntries(heroes.map((h) => [h.id, h]));
}
// Aplana las listas de mapas y quita duplicados con un Set.
function mapasUnicos(heroes) {
return [...new Set(heroes.flatMap((h) => h.mapas))];
}
function mostrar() {
const resumen = resumenPorRol(roster);
const indice = indicePorId(roster);
const mapas = mapasUnicos(roster);
const resumenTexto = Object.entries(resumen)
.map(([rol, n]) => `${rol}: ${n}`)
.join(" · ");
// imprime los resultados de las tres operaciones por consola
console.log("Resumen por rol: " + resumenTexto);
console.log(
"Buscar por id (h3) y último del roster: " +
indice["h3"].nombre +
" · último: " +
roster.at(-1).nombre,
);
console.log("Mapas únicos (" + mapas.length + "): " + mapas.join(", "));
}
mostrar(); Por qué es mejor que el anterior
- resumenPorRol encadena el pipeline entries → map → fromEntries para transformar el agrupado en un resumen { rol: nº }.
- mapasUnicos combina flatMap con un Set (cap. de Sets y Maps): aplana y deduplica en una expresión.
- Funciones puras y render que solo presenta; usa at(-1) para el último del roster.