learning-front

Nivel 2 · JavaScript: fundamentos del lenguaje

Arrays y objetos

Las dos estructuras que sostienen casi todos los datos del frontend: cómo crearlas, recorrerlas, modificarlas, copiarlas y desempaquetarlas sin sorpresas.

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:

javascript
// array de tres strings
const roles = ['Tanque', 'Daño', 'Apoyo'];

Cada elemento tiene un índice numérico que empieza en 0:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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