Cuando el backend te manda datos, casi siempre llegan en una de estas dos formas: un array —una lista ordenada de elementos— o un objeto —un conjunto de propiedades con nombre—. Y en la práctica, lo normal es que vengan mezclados: un array de objetos, u objetos con arrays dentro.
Saber crear, leer, modificar y copiar estas estructuras es lo más básico del
trabajo diario en frontend. Este capítulo lo cubre todo; los métodos que las
transforman de forma declarativa (map, filter, reduce) llegan en el siguiente.
Arrays#
Un array es una lista ordenada de valores. Se declara con corchetes y sus elementos se separan con comas:
// array de tres strings
const roles = ['Tanque', 'Daño', 'Apoyo'];Cada elemento tiene un índice numérico que empieza en 0:
// 'Tanque' — índice 0: el primero
console.log(roles[0]);
// 'Apoyo' — índice 2: el tercero
console.log(roles[2]);La propiedad length da el número de elementos. Para leer el último sin saber
cuántos hay:
// 3 — cuántos elementos tiene el array
console.log(roles.length);
// 'Apoyo' — último elemento (length - 1 = índice final)
console.log(roles[roles.length - 1]);Añadir y quitar elementos#
Los métodos más usados para modificar el propio array:
push(valor)— añade al final.pop()— elimina y devuelve el último.unshift(valor)— añade al principio.shift()— elimina y devuelve el primero.
push y pop operan en la cola del array:
// array con dos elementos
const lista = ['Tracer', 'Mercy'];
// añade al final → ['Tracer', 'Mercy', 'Reinhardt']
lista.push('Reinhardt');
// elimina y devuelve el último → ultimo = 'Reinhardt'
const ultimo = lista.pop();
// ['Tracer', 'Mercy'] — volvemos al punto de partida
console.log(lista);unshift y shift operan en la cabeza del array:
// array con dos elementos
const lista = ['Tracer', 'Mercy'];
// añade al principio → ['Reinhardt', 'Tracer', 'Mercy']
lista.unshift('Reinhardt');
// elimina y devuelve el primero → primero = 'Reinhardt'
const primero = lista.shift();
// ['Tracer', 'Mercy'] — volvemos al punto de partida
console.log(lista);Ten en cuenta que estos cuatro métodos modifican el array en su sitio. Ya verás por qué eso puede ser un problema cuando el mismo array lo usan varios sitios del código.
Recorrer con for...of#
La forma más directa de recorrer un array elemento a elemento es for...of,
que ya conoces del capítulo de condicionales y bucles:
// array de tres nombres
const heroes = ['Tracer', 'Reinhardt', 'Mercy'];
// en cada iteración, nombre toma el valor del elemento actual
for (const nombre of heroes) {
// imprime 'Héroe: Tracer', luego 'Héroe: Reinhardt', luego 'Héroe: Mercy'
console.log('Héroe: ' + nombre);
}Objetos#
Un objeto agrupa datos relacionados bajo un nombre. Se declara con llaves; cada
par clave: valor es una propiedad:
// literal de objeto: empieza con { y termina con }
const heroe = {
// propiedad 'nombre' con valor string
nombre: 'Tracer',
// propiedad 'rol' con valor string
rol: 'Daño',
// propiedad 'partidas' con valor numérico
partidas: 120,
// propiedad 'victorias' con valor numérico
victorias: 78,
};Leer propiedades: punto y corchetes#
Tienes dos formas de acceder a una propiedad:
// Con punto: la forma normal cuando el nombre de la propiedad es fijo y conocido.
// 'Tracer' — accedemos a la propiedad 'nombre'
console.log(heroe.nombre);
// 'Daño' — accedemos a la propiedad 'rol'
console.log(heroe.rol);
// Con corchetes: imprescindible cuando el nombre viene de una variable.
// el nombre de la propiedad que queremos leer
const campo = 'partidas';
// 120 — equivale a heroe.partidas, pero el nombre viene de una variable
console.log(heroe[campo]);Los corchetes también sirven cuando el nombre de la propiedad tiene espacios o caracteres especiales, aunque eso es raro en código bien escrito.
Objetos con arrays (u otros objetos) dentro#
El valor de una propiedad puede ser cualquier cosa, incluido otro array u otro
objeto. Así llegan casi siempre los datos reales: un héroe no son solo cuatro
campos planos, también trae su lista de habilidades y un bloque de estadísticas.
Para leer lo que hay dentro, encadenas accesos: cada . o [] baja un nivel.
// objeto con un array dentro (habilidades) y un objeto dentro (stats)
const heroe = {
nombre: 'Tracer',
rol: 'Daño',
// propiedad cuyo valor es un ARRAY de strings
habilidades: ['Pulso', 'Parpadeo', 'Retroceso'],
// propiedad cuyo valor es otro OBJETO
stats: { partidas: 120, victorias: 78 },
};
// Para entrar al array: primero la propiedad, luego el índice.
// 'Pulso' — heroe.habilidades es el array; [0] coge su primer elemento
console.log(heroe.habilidades[0]);
// 3 — el array de habilidades tiene tres elementos
console.log(heroe.habilidades.length);
// Para entrar al objeto interno: encadenas puntos.
// 78 — primero la propiedad stats, luego su propiedad victorias
console.log(heroe.stats.victorias);En el Nivel 3 verás formas más cómodas de sacar varios valores anidados de golpe;
por ahora, encadenar accesos con . y [] es todo lo que necesitas.
Añadir, cambiar y borrar propiedades#
// Añadir o cambiar: asignación directa sobre la propiedad.
// crea la propiedad 'winrate' y le asigna el resultado
heroe.winrate = heroe.victorias / heroe.partidas;
// Borrar: con delete.
// elimina la propiedad 'winrate' del objeto
delete heroe.winrate;Para saber si una propiedad existe en un objeto puedes usar el operador in.
Escribe 'nombre' in objeto y devuelve true si la propiedad existe, false si no.
Es distinto del in que ya conoces de for...of: ahí in no aparece; la palabra clave
del bucle es of. Aquí, en cambio, in comprueba presencia y produce un booleano:
// true — la propiedad 'nombre' existe en heroe
console.log('nombre' in heroe);
// false — la acabamos de borrar con delete, ya no está
console.log('winrate' in heroe);Desestructuración de arrays#
Cuando tienes un array y quieres sacar sus elementos a variables individuales,
puedes hacerlo por índice (par[0], par[1]), pero JavaScript tiene una forma
más directa: la desestructuración. Con const [a, b] = miArray declaras
dos variables de golpe; a recibe el elemento de índice 0 y b el de índice 1,
por posición:
// un array de dos elementos
const par = ['rol', 'Tanque'];
// desestructurar: clave recibe 'rol' (índice 0), valor recibe 'Tanque' (índice 1)
const [clave, valor] = par;
// rol: Tanque
console.log(clave + ': ' + valor);Esta sintaxis es clave para el siguiente apartado: Object.entries devuelve un
array de pares [clave, valor], y la desestructuración permite repartirlos en
dos variables en la misma línea del for...of.
Recorrer un objeto#
Los objetos no tienen índices numéricos, pero Object.keys, Object.values y
Object.entries te dan sus contenidos como arrays, que ya sí puedes recorrer:
// objeto de estadísticas
const stats = { partidas: 120, victorias: 78, derrotas: 42 };
// Solo los nombres de las propiedades: devuelve un array de strings.
// ['partidas', 'victorias', 'derrotas']
console.log(Object.keys(stats));
// Solo los valores: devuelve un array con los valores en el mismo orden.
// [120, 78, 42]
console.log(Object.values(stats));
// Pares [clave, valor]: el más versátil, permite usar ambos a la vez.
// Cada par que devuelve Object.entries es un array de dos elementos; lo desestructuramos
// en clave y valor directamente en la cabecera del for...of (como vimos justo arriba).
// desestructuramos cada par [clave, valor] del array
for (const [clave, valor] of Object.entries(stats)) {
// imprime 'partidas: 120', luego 'victorias: 78', etc.
console.log(clave + ': ' + valor);
}Destructuring de objetos#
Cuando tienes un objeto y quieres usar varias de sus propiedades, puedes acceder
a ellas una por una con el punto (heroe.nombre, heroe.rol…). Pero si vas a
usarlas muchas veces, hay una forma más directa: el destructuring. Con
const { nombre, rol } = heroe creas las dos variables de golpe; cada nombre
debe coincidir con el de la clave en el objeto.
// objeto con cuatro propiedades
const heroe = {
nombre: 'Tracer',
rol: 'Daño',
partidas: 120,
victorias: 78,
};
// Destructuring: crea las variables nombre y rol en una línea.
// equivale exactamente a:
// const nombre = heroe.nombre;
// const rol = heroe.rol;
const { nombre, rol } = heroe;
// Tracer (Daño)
console.log(nombre + ' (' + rol + ')');El orden dentro de las llaves no importa: se empareja por nombre de clave, no
por posición. const { rol, nombre } = heroe da exactamente lo mismo.
Renombrar al desempaquetar#
A veces el nombre de la clave no te sirve: ya tienes una variable con ese nombre,
o quieres algo más descriptivo. La sintaxis clave: nombreNuevo lo resuelve.
// objeto héroe
const heroe = { nombre: 'Tracer', rol: 'Daño' };
// Saca heroe.nombre pero lo guarda en una variable llamada nombreHeroe.
// Lectura: "de la clave nombre, dame una variable nombreHeroe".
const { nombre: nombreHeroe, rol } = heroe;
// la variable 'nombre' NO existe; la renombramos a nombreHeroe
// Tracer (Daño)
console.log(nombreHeroe + ' (' + rol + ')');Valor por defecto#
Si una propiedad no existe en el objeto, su variable sería undefined. Puedes
poner un valor por defecto que se usa solo cuando la propiedad falta:
// héroe sin la propiedad 'rango'
const heroe = { nombre: 'Genji', rol: 'Daño' };
// Sin defecto: rango sería undefined.
// Con defecto: si la clave falta, se usa 'Sin clasificar'.
const { nombre, rango = 'Sin clasificar' } = heroe;
// Sin clasificar
console.log(rango);
// Si la propiedad sí existe, el defecto se ignora y gana el valor real.
const otro = { nombre: 'Tracer', rango: 'Diamante' };
const { rango: rangoOtro = 'Sin clasificar' } = otro;
// Diamante
console.log(rangoOtro);En el Nivel 3 verás cómo bajar más de un nivel (destructuring anidado) y cómo destructurar directamente en los parámetros de una función.
Arrays de objetos: el formato real#
En la práctica, los datos casi siempre llegan como un array de objetos. El dataset del Team Builder tiene exactamente esa forma:
// array que contiene objetos
const heroes = [
// objeto héroe en índice 0
{ nombre: 'Tracer', rol: 'Daño', partidas: 120, victorias: 78 },
// objeto héroe en índice 1
{ nombre: 'Reinhardt', rol: 'Tanque', partidas: 90, victorias: 51 },
// objeto héroe en índice 2
{ nombre: 'Mercy', rol: 'Apoyo', partidas: 200, victorias: 130 },
];Para recorrerlo y leer propiedades de cada elemento:
// heroe es cada objeto del array en cada iteración
for (const heroe of heroes) {
// accedemos a las propiedades del objeto con punto
console.log(`${heroe.nombre} (${heroe.rol})`);
}
// imprime: 'Tracer (Daño)', 'Reinhardt (Tanque)', 'Mercy (Apoyo)'Mutación y referencias#
Aquí hay un detalle que sorprende a casi todo el mundo la primera vez. Cuando asignas un objeto a otra variable, no copias el objeto: las dos variables apuntan al mismo sitio en memoria:
// creamos un objeto con una propiedad x = 1
const a = { x: 1 };
// b NO es una copia: b y a apuntan al MISMO objeto en memoria
const b = a;
// modificamos x a través de b
b.x = 99;
// 99, no 1 — a refleja el cambio porque apunta al mismo objeto
console.log(a.x);Lo mismo pasa con los arrays. Esto se llama mutación: modificar el contenido original a través de cualquier referencia al mismo.
Ya viste este concepto de forma breve en el capítulo de valores y comparaciones. Ahora que trabajas con objetos y arrays todo el tiempo, se vuelve central.
En una aplicación real, varios sitios del código pueden tener referencias al mismo array o al mismo objeto. Si uno lo muta, los demás lo ven cambiado sin esperarlo, y eso genera bugs difíciles de rastrear.
Copiar con spread#
La solución es crear una copia en lugar de una referencia. El operador
spread (...) es la forma más directa:
// objeto héroe de ejemplo
const heroe = { nombre: 'Tracer', rol: 'Daño', partidas: 120, victorias: 78 };
// array de héroes de ejemplo
const heroes = [
{ nombre: 'Tracer', rol: 'Daño', partidas: 120, victorias: 78 },
{ nombre: 'Reinhardt', rol: 'Tanque', partidas: 90, victorias: 51 },
];
// Copia de un objeto: el spread ... expande todas las propiedades en el nuevo objeto.
// copia es un objeto nuevo con los mismos datos que heroe
const copia = { ...heroe };
// modificamos solo la copia; heroe.rol sigue siendo 'Daño'
copia.rol = 'Tanque';
// Copia y añade o pisa un campo al mismo tiempo.
// Las propiedades escritas DESPUÉS del spread sobreescriben las del spread si tienen el mismo nombre.
// nuevo objeto con todas las props de heroe más winrate
const enriquecido = { ...heroe, winrate: heroe.victorias / heroe.partidas };
// nuevo objeto con todas las props de heroe pero con rol cambiado
const comoTanque = { ...heroe, rol: 'Tanque' };
// heroe sigue igual; solo cambió la copia
console.log('Original: ' + heroe.rol);
// Daño (intacto)
console.log('Copia: ' + comoTanque.rol);
// Tanque
// Copia de un array: el spread también funciona dentro de corchetes.
// nuevo array con los mismos elementos que heroes
const otraLista = [...heroes];Fusionar objetos con spread#
Cuando tienes dos objetos y quieres combinar sus campos en uno nuevo, puedes extender ambos con spread. Si una clave aparece en los dos, gana la última:
// plantilla base de un equipo
const base = { region: 'Europa', modo: 'Competitivo', maxJugadores: 6 };
// cambios que quiero aplicar encima
const cambios = { modo: 'Amistoso' };
// Fusión: copia base y luego aplica cambios encima.
// 'modo' está en ambos → gana el de cambios (está después).
const plantilla = { ...base, ...cambios };
// Amistoso (ganó el de cambios)
console.log('Modo: ' + plantilla.modo);
// Europa (viene de base, no estaba en cambios)
console.log('Región: ' + plantilla.region);Combinar arrays con spread#
El spread de arrays vuelca todos sus elementos en el nuevo array. Sirve para concatenar listas o insertar elementos en medio, siempre creando un array nuevo:
// dos subgrupos
const tanques = ['Reinhardt', 'Winston'];
const apoyos = ['Mercy', 'Ana'];
// Combinar: vuelca los elementos de ambos en un array nuevo.
// ['Reinhardt', 'Winston', 'Mercy', 'Ana']
const plantilla = [...tanques, ...apoyos];
// .join(', ') pega los elementos del array en UN string, separados por lo que le pases.
// Aquí, ', ' → 'Reinhardt, Winston, Mercy, Ana'. Útil para mostrar una lista en una línea.
console.log(plantilla.join(', '));
// Insertar en medio: pones elementos sueltos entre los spreads.
// ['Reinhardt', 'Winston', 'Tracer', 'Mercy', 'Ana']
const conDano = [...tanques, 'Tracer', ...apoyos];
// otra vez .join, ahora con el array que tiene a Tracer en medio
console.log(conDano.join(', '));El spread hace una copia superficial: copia las propiedades del objeto un nivel hacia abajo, pero si alguna de esas propiedades fuera a su vez un objeto, la copia y el original seguirían compartiendo esa parte interior. Para datasets planos como el del Team Builder eso no supone ningún problema.
La idea de trabajar siempre con copias en lugar de modificar los datos
originales se llama inmutabilidad. No es obligatoria, pero evita toda una
clase de bugs y es la disciplina estándar en el frontend moderno. En el
próximo capítulo, cuando veas map y filter, verás que ambos la aplican
por diseño: siempre crean arrays nuevos, nunca tocan el original.
Pruébalo tú#
El playground recorre el dataset de héroes, desempaqueta propiedades con
destructuring, crea copias enriquecidas con spread, fusiona objetos y recorre
un objeto con Object.entries. Cambia algún valor y observa cómo se propaga.
Comprueba lo que sabes#
Pregunta 1 de 6
¿Cuál es el índice del primer elemento de un array?
Tu turno#
Tienes el roster completo. Sin map, filter ni reduce —eso llega en el
próximo capítulo—, calcula los winrates, encuentra al mejor héroe y agrupa
por rol. Cuando lo tengas, despliega las soluciones y fíjate en el salto de
un nivel al siguiente.
Ejercicio · en esta página
Analiza el roster de héroes
A partir del array de héroes, construye un nuevo array con el winrate calculado, encuentra el héroe con mayor winrate y agrupa los nombres por rol. Sin map, filter ni reduce: solo for...of, spread y destructuring básico.
Paso 1: Que funcione
- conWinrate contiene todos los héroes con su winrate.
- mejorHeroe es el héroe con el winrate más alto.
- porRol agrupa los nombres por rol correctamente.
- Los tres resultados se imprimen con console.log.
Paso 2: Que esté pulido
- Los objetos originales no se modifican: usas spread para las copias.
- El winrate no se recalcula: lo lees de conWinrate donde ya está.
- La búsqueda del mejor héroe maneja el array vacío sin reventar.
- Usas destructuring de objeto en al menos un punto del código (por ejemplo, al imprimir el mejor héroe).
Paso 3: Que sea excelente
- Cada operación vive en su propia función pura y reutilizable.
- Inmutabilidad total: ninguna función modifica sus argumentos.
- Usas destructuring en los parámetros o al leer los resultados.
- Un comentario forward-reference explica qué hará map/reduce en el próximo capítulo.
Ver soluciones
// ════════════════════════════════════════════════════════════════════════════
// NIVEL OK — "funciona"
//
// Resuelve los tres puntos del enunciado con bucles for...of.
// Funciona y los resultados son correctos: es el primer requisito.
//
// Sus límites (los que arregla el nivel "Mejor"):
// - Muta los objetos originales al añadir `winrate` directamente sobre `heroe`
// en vez de crear copias. Si alguien usara `heroes` después, los objetos
// ya llevarían el campo winrate sin haberlo pedido.
// - El acceso a `heroe.victorias / heroe.partidas` se repite en el bucle
// del mejor héroe: el winrate ya está en conWinrate, pero se recalcula.
// - La inicialización de `mejorHeroe` con índice manual es frágil
// (si el array estuviera vacío, `heroes[0]` reventaría).
// Aun así: los tres console.log imprimen lo correcto. Eso vale como OK.
// ════════════════════════════════════════════════════════════════════════════
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 },
];
// Añade winrate directamente sobre cada objeto original (mutación).
// array vacío que iremos llenando
const conWinrate = [];
for (const heroe of heroes) {
// heroe es cada objeto en cada iteración
// MUTACIÓN: añadimos winrate al objeto original
heroe.winrate = heroe.victorias / heroe.partidas;
// push añade la referencia al mismo objeto (no una copia)
conWinrate.push(heroe);
}
// array de héroes con winrate añadido
console.log(conWinrate);
// Recalcula el winrate para encontrar el mejor.
// partimos del primero como candidato inicial (frágil si el array está vacío)
let mejorHeroe = heroes[0];
for (const heroe of conWinrate) {
// recorremos el array con los winrates
// recalcula el winrate en vez de leer el campo
if (
heroe.victorias / heroe.partidas >
mejorHeroe.victorias / mejorHeroe.partidas
) {
// actualizamos el candidato si encontramos uno mejor
mejorHeroe = heroe;
}
}
// héroe con mayor winrate
console.log(mejorHeroe);
// Agrupa por rol.
// objeto vacío donde guardaremos un array de nombres por cada rol
const porRol = {};
for (const heroe of heroes) {
// recorremos el array original
// si este rol no tiene array todavía
if (porRol[heroe.rol] === undefined) {
// creamos el array para ese rol
porRol[heroe.rol] = [];
}
// añadimos el nombre del héroe al array de su rol
porRol[heroe.rol].push(heroe.nombre);
}
// { Daño: [...], Tanque: [...], Apoyo: [...] }
console.log(porRol); Por qué este nivel
- Resuelve los tres puntos del enunciado y los resultados son correctos: eso vale como OK.
- Pero muta los objetos originales añadiendo `winrate` directamente sobre cada `heroe` en vez de crear copias con spread.
- El winrate se recalcula en el bucle del mejor héroe aunque ya está disponible en `conWinrate`.
- La inicialización `mejorHeroe = heroes[0]` revienta si el array estuviera vacío.
// ════════════════════════════════════════════════════════════════════════════
// NIVEL MEJOR — "pulido"
//
// Misma lógica que OK, pero con cuatro mejoras concretas:
// 1. No muta los objetos originales: el spread `{ ...heroe, winrate }` crea
// un objeto nuevo. Al final del script, `heroes` sigue intacto.
// 2. El winrate no se recalcula: el bucle del mejor héroe lee el campo ya
// calculado en `conWinrate`, en lugar de volver a dividir.
// 3. La inicialización de `mejorHeroe` parte de `conWinrate[0]` y el bucle
// maneja el array vacío con un guard inicial explícito.
// 4. Usa destructuring al imprimir el resultado final: en vez de repetir
// `mejorHeroe.nombre` y `mejorHeroe.winrate`, los desempaqueta primero.
//
// Todavía tiene un punto de mejora: los datos (héroes) y la presentación
// (qué imprimimos) están mezclados en el mismo flujo. Eso lo resuelve
// el nivel Excelente.
// ════════════════════════════════════════════════════════════════════════════
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 },
];
// Crea objetos nuevos con spread: heroes permanece sin tocar.
// array vacío que iremos llenando con copias enriquecidas
const conWinrate = [];
for (const heroe of heroes) {
// heroe es cada objeto del array
// spread crea un objeto nuevo; winrate se añade solo a la copia
conWinrate.push({ ...heroe, winrate: heroe.victorias / heroe.partidas });
}
// array de héroes enriquecidos (heroes no ha cambiado)
console.log(conWinrate);
// Lee el winrate ya calculado; maneja el caso de lista vacía.
// si el array está vacío, conWinrate[0] es undefined → null
let mejorHeroe = conWinrate[0] || null;
for (const heroe of conWinrate) {
// recorremos el array con los winrates ya calculados
// comparamos el campo winrate (ya está calculado, no hay que repetir la división)
if (heroe.winrate > mejorHeroe.winrate) {
// actualizamos el candidato si encontramos uno con mayor winrate
mejorHeroe = heroe;
}
}
// Destructuring al imprimir: saca nombre y winrate del objeto en una línea.
const { nombre: nombreMejor, winrate: winrateMejor } = mejorHeroe;
// héroe con mayor winrate
console.log(
"Mejor héroe: " + nombreMejor + " (" + winrateMejor.toFixed(2) + ")",
);
// Agrupa por rol; usa || (cortocircuito) para inicializar el array si no existe.
// objeto vacío donde guardaremos un array de nombres por cada rol
const porRol = {};
for (const heroe of conWinrate) {
// recorremos el array enriquecido
// si el rol no tiene array, lo crea; si ya existe, lo deja
porRol[heroe.rol] = porRol[heroe.rol] || [];
// añade el nombre del héroe al array de su rol
porRol[heroe.rol].push(heroe.nombre);
}
// { Daño: [...], Tanque: [...], Apoyo: [...] }
console.log(porRol); Por qué es mejor que el anterior
- El spread `{ ...heroe, winrate }` crea objetos nuevos: el array `heroes` original sale de la función intacto.
- El bucle del mejor héroe lee el campo ya calculado, sin volver a dividir victorias entre partidas.
- El guard `conWinrate[0] || null` evita el crash con lista vacía.
- Usa destructuring al imprimir el resultado: `const { nombre, winrate }` es más limpio que repetir `mejorHeroe.nombre` y `mejorHeroe.winrate`.
// ════════════════════════════════════════════════════════════════════════════
// NIVEL EXCELENTE — "óptimo"
//
// Separa los datos de la presentación: tres funciones puras calculan los
// resultados; al final solo hay console.log que los leen.
//
// Por qué importa la separación:
// - Si mañana necesitas el ranking en otra parte del código, llamas a
// `enriquecerHeroes` sin arrastrar los console.log.
// - Cada función es pura: misma entrada → misma salida, sin efectos fuera
// de ella. Son triviales de probar y de entender de forma aislada.
// - La inmutabilidad es total: ninguna función modifica sus argumentos.
// - Usa destructuring en los parámetros: la firma de cada función declara
// qué campos necesita; dentro no se repite `heroe.`.
//
// Forward-reference: en el próximo capítulo verás cómo `map` y `reduce`
// sustituyen los bucles `for...of` de estas funciones con código aún más
// corto y declarativo. La lógica es exactamente la misma; cambia la forma
// de escribirla.
// ════════════════════════════════════════════════════════════════════════════
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 },
];
// Enriquece cada héroe con su winrate. Devuelve un array nuevo; no toca la entrada.
// Destructuring en el parámetro del bucle: declaramos qué campos usamos.
function enriquecerHeroes(lista) {
// recibe el array original como argumento
const resultado = [];
// array vacío donde construiremos las copias
for (const heroe of lista) {
// heroe es cada objeto del array en cada iteración
// destructuring: sacamos victorias y partidas del heroe actual
const { victorias, partidas } = heroe;
// spread crea copia; winrate solo en la copia
resultado.push({ ...heroe, winrate: victorias / partidas });
}
// devolvemos el array nuevo; lista no se ha modificado
return resultado;
}
// Devuelve el héroe con mayor winrate, o null si la lista está vacía.
function encontrarMejor(lista) {
// guard: si la lista está vacía, devolvemos null en vez de reventar
if (lista.length === 0) return null;
// partimos del primero como candidato inicial
let mejor = lista[0];
for (const heroe of lista) {
// recorremos todo el array (el primer heroe se compara consigo mismo, sin daño)
// comparamos el campo winrate ya calculado
if (heroe.winrate > mejor.winrate) {
// actualizamos el candidato si encontramos uno mejor
mejor = heroe;
}
}
// devolvemos el objeto héroe con mayor winrate
return mejor;
}
// Agrupa por rol: devuelve { Tanque: [nombres], Daño: [...], Apoyo: [...] }.
function agruparPorRol(lista) {
// objeto vacío donde construiremos los grupos
const grupos = {};
for (const heroe of lista) {
// destructuring: sacamos nombre y rol del héroe actual
const { nombre, rol } = heroe;
// si el rol no tiene array todavía, lo inicializa
grupos[rol] = grupos[rol] || [];
// añade el nombre del héroe al array de su rol
grupos[rol].push(nombre);
}
// devolvemos el objeto con los grupos; lista no se ha modificado
return grupos;
}
// ── Presentación ─────────────────────────────────────────────────────────────
// Las funciones calculan; aquí solo leemos y mostramos.
// llamamos a la función con el array original
const conWinrate = enriquecerHeroes(heroes);
// buscamos el mejor entre los héroes enriquecidos
const mejorHeroe = encontrarMejor(conWinrate);
// agrupamos el array original (sin necesitar winrate)
const porRol = agruparPorRol(heroes);
// héroes con winrate calculado
console.log(conWinrate);
// Destructuring al leer el resultado: saca nombre y winrate en una línea.
const { nombre: nombreMejor, winrate: winrateMejor } = mejorHeroe;
// el héroe con mayor winrate
console.log("Mejor: " + nombreMejor + " (" + winrateMejor.toFixed(2) + ")");
// héroes agrupados por rol
console.log(porRol); Por qué es mejor que el anterior
- Tres funciones puras: misma entrada, misma salida, sin efectos fuera de ellas. Son triviales de probar.
- La sección de "presentación" solo lee resultados ya calculados: separa el qué del cómo.
- Inmutabilidad total en todos los niveles: ni los objetos individuales ni el array se tocan.
- Destructuring en los parámetros de las funciones puras: la firma declara qué campos usa.
- El comentario forward-reference prepara al alumno para map/filter/reduce del próximo capítulo, sin spoilear la sintaxis.