← JavaScript: el lenguaje

Sesión 4

Arrays y objetos

Tres horas. Media hora para entender qué devuelve cada método de array y cómo se modela una entidad con un objeto, y dos horas y media convirtiendo tus listas paralelas en el catálogo que usarás el resto del módulo.

Hoy · Hoja de ruta

  1. 1. Aprende: Cómo se guarda una lista, qué métodos la modifican y cuáles devuelven otra, qué devuelve exactamente map, filter, find y reduce, y qué es JSON.
  2. 2. Haz: Construye tu catálogo como array de objetos, consúltalo con métodos declarativos y conviértelo a JSON y de vuelta.
  3. 3. Comprueba: Eliges el método por lo que devuelve, y el catálogo original nunca queda alterado sin que lo hayas querido.

Antes de empezar · 5 minutos, sin apuntes

  1. En la sesión 2 usaste tres listas paralelas. ¿Qué problema tiene eso?
  2. ¿Qué pasa si pides el elemento número cien de una lista de tres?
  3. En la sesión 3 escribiste aplicarATodos y filtrarPor. ¿Qué devolvía cada una?

Se explica

25 minutos · conceptos y demostración

Hoy hay dos preguntas que se repiten en cada línea: ¿esto modifica el original o devuelve algo nuevo? y ¿qué me devuelve exactamente este método?. Casi todos los errores de la sesión son una de las dos mal contestada.

Una lista ordenada de valores

const categorias = ["teclados", "ratones", "monitores"];

categorias[0]           // "teclados"   ← se empieza a contar en cero
categorias.length       // 3
categorias[99]          // undefined    ← no da error
categorias.at(-1)       // "monitores"  ← el último, sin calcular índices

Que pedir un índice inexistente devuelva undefined en lugar de fallar es cómodo y peligroso a partes iguales: el programa sigue, y el undefined viaja hasta reventar tres funciones más allá.

Distingue mutar de devolver

Algunos métodos cambian el array original —push, pop, splice, sort, reverse— y otros devuelven uno nuevo sin tocarlo: slice, concat, map, filter.

El error clásico: ordenar «una copia» que en realidad era el mismo array, y descubrir después que el catálogo original quedó desordenado para todo el programa. Ante la duda, copia primero.

const original = [3, 1, 2];

const ordenado = [...original].sort();   // copia y ordena la copia
original;                                // [3, 1, 2], intacto

Los tres puntos, el operador de propagación, copian los elementos en un array nuevo.

Cada método devuelve una cosa distinta

Método Devuelve Pregunta que responde
map Un array del mismo tamaño «Transforma cada uno»
filter Un array más corto «Quédate con los que cumplan»
find Un elemento, o undefined «Dame el primero que cumpla»
findIndex Una posición, o -1 «¿En qué posición está?»
some true o false «¿Hay alguno que cumpla?»
every true o false «¿Cumplen todos?»
sort El array ordenado «Ponlos en este orden»
reduce Un solo valor «Combínalos todos en uno»
forEach Nada «Haz algo con cada uno»

Elige por lo que necesitas recibir

La mitad de los errores con estos métodos son de elección, no de escritura: usar filter y quedarse con [0] cuando lo que se quería era find, o usar find esperando una lista.

Pregúntate qué quieres tener después: ¿otra lista, un elemento, un sí o un no, o un número?

const productos = [
  { nombre: "Teclado", precio: 89.9, stock: 4 },
  { nombre: "Ratón", precio: 24.5, stock: 0 },
  { nombre: "Monitor", precio: 199, stock: 7 }
];

const nombres = productos.map((p) => p.nombre);
const disponibles = productos.filter((p) => p.stock > 0);
const caro = productos.find((p) => p.precio > 100);
const hayAgotados = productos.some((p) => p.stock === 0);
const todosBaratos = productos.every((p) => p.precio < 500);

sort y su trampa

[10, 9, 100].sort();                  // [10, 100, 9]  ← compara como texto
[10, 9, 100].sort((a, b) => a - b);   // [9, 10, 100]

Sin función de comparación, sort convierte a texto. Con números hay que pasarla siempre: a - b ascendente, b - a descendente. Para textos con acentos y mayúsculas:

productos.sort((a, b) => a.nombre.localeCompare(b.nombre));

Recuerda además que sort muta: ordena sobre una copia si el orden original importa.

reduce es el que da más miedo y el que menos lo merece:

const total = productos.reduce((suma, p) => suma + p.precio * p.stock, 0);

Se lee así: empieza con 0, y por cada producto, quédate con lo que llevabas más lo que aporta este. Es el acumulador de la sesión 2, escrito en una línea.

Como cada método devuelve un array, el siguiente puede trabajar sobre él:

const resumen = productos
  .filter((p) => p.stock > 0)
  .sort((a, b) => a.precio - b.precio)
  .map((p) => `${p.nombre}: ${p.precio.toFixed(2)} €`);

Se lee de arriba abajo como una frase: filtra, ordena, formatea. Con sort en medio conviene fijarse: aquí es seguro porque filter ya devolvió un array nuevo.

Un objeto agrupa datos con nombre

const producto = {
  id: 1,
  nombre: "Teclado mecánico",
  precio: 89.9,
  categoria: "teclados",
  stock: 4,
  activo: true
};

producto.nombre           // "Teclado mecánico"
producto["precio"]        // 89.9, útil cuando la clave es una variable
producto.color            // undefined

Un catálogo, por tanto, es un array de objetos:

const catalogo = [
  { id: 1, nombre: "Teclado", precio: 89.9, stock: 4 },
  { id: 2, nombre: "Ratón", precio: 24.5, stock: 0 }
];

Esta es la estructura que vas a usar durante el resto del módulo: en la UD4 la pintarás en la página, en la UD5 la guardarás en un fichero y en la UD6 la servirá tu API.

Una const con un objeto sí se puede modificar por dentro

const protege la asignación, no el contenido. Puedes cambiar las propiedades de un objeto declarado con const; lo que no puedes es apuntarlo a otro objeto distinto.

De ahí sale otro clásico: dos variables que apuntan al mismo objeto. Modificar una cambia «las dos», porque siempre fue una. Para copiar de verdad: { ...producto }.

Al acceder a datos anidados aparece el fallo más habitual con datos ajenos, y su remedio:

const pedido = {
  id: 1001,
  cliente: { nombre: "Ana", email: "ana@ejemplo.com" },
  lineas: [{ producto: "Teclado", unidades: 1 }]
};

pedido.cliente.nombre            // "Ana"
pedido.lineas[0].unidades        // 1
pedido.envio.direccion           // TypeError: no se puede leer de undefined
pedido.envio?.direccion          // undefined, sin romper nada

El interrogante es el encadenamiento opcional: si lo de la izquierda no existe, devuelve undefined en lugar de lanzar un error.

Para sacar las propiedades que necesitas y darles nombre está la desestructuración, que ahorra repetir producto. diez veces y hace explícito qué usa cada función:

const { nombre, precio } = producto;
const [primero, segundo] = catalogo;

function describir({ nombre, precio }) {
  return `${nombre} · ${precio.toFixed(2)} €`;
}

JSON, el formato de intercambio

JSON

Un formato de texto para representar datos, nacido de la sintaxis de los objetos de JavaScript pero independiente de él: lo entienden Java, Python, PHP y cualquier otro lenguaje. Es como viajan los datos entre un navegador y un servidor.

const texto = JSON.stringify(producto);           // objeto → texto
const texto2 = JSON.stringify(producto, null, 2); // con sangría, legible
const recuperado = JSON.parse(texto);             // texto → objeto

Sus reglas son más estrictas que las de JavaScript:

{
  "nombre": "Teclado mecánico",
  "precio": 89.9,
  "stock": 4,
  "activo": true,
  "etiquetas": ["mecánico", "retroiluminado"]
}

Las claves van siempre entre comillas dobles, no se admiten comillas simples, ni comas finales, ni comentarios, ni funciones, ni undefined. Un JSON.parse de un texto mal formado lanza un error: en la sesión 5 lo trataremos como se debe.

Se trabaja

150 minutos · práctica sobre tu propio proyecto

Hoy el catálogo deja de ser un conjunto de listas sueltas y pasa a ser la estructura de datos del resto del módulo. El último paso cierra el círculo abierto en la sesión 3.

Paso 1 · El catálogo como lista · 30 min

Crea js/datos.js:

  1. Declara un array con al menos ocho nombres de producto de tu tema.
  2. Otro con sus precios, en el mismo orden.
  3. Escribe funciones para añadir, quitar y buscar un producto.
  4. Ordena una copia por precio sin alterar el original, y demuéstralo imprimiendo los dos.
  5. Escribe una función que devuelva los tres primeros.
  6. Escribe una función que devuelva el array sin duplicados.
Consultar · añadir, quitar y buscar
const lista = ["teclados", "ratones"];

lista.push("monitores");      // añade al final       → 3
lista.pop();                  // quita el último      → "monitores"
lista.unshift("cables");      // añade al principio
lista.shift();                // quita el primero

lista.includes("ratones");    // true
lista.indexOf("ratones");     // 1, o -1 si no está

Para recorrer haciendo algo con cada elemento, sin obtener otra lista:

categorias.forEach((categoria, indice) => {
  console.log(`${indice + 1}. ${categoria}`);
});

Antes de continuar: después de ordenar la copia, el array original imprime exactamente el mismo orden que tenía al declararlo.

Paso 2 · Consultas del catálogo · 40 min

En js/catalogo.js, sobre tu lista de productos:

  1. nombresDisponibles(productos) con filter y map.
  2. buscarPorNombre(productos, texto) con find, sin distinguir mayúsculas.
  3. hayAgotados(productos) con some.
  4. valorAlmacen(productos) con reduce.
  5. ordenarPorPrecio(productos, ascendente) que no mute el original.
  6. Una consulta encadenada que responda a una pregunta compuesta de tu tema.

Para cada función, escribe en un comentario qué devuelve antes de programarla. Si no sabes decirlo, todavía no sabes qué método usar.

Paso 3 · El catálogo de verdad · 40 min

Es el trabajo central de la sesión. Reescribe js/datos.js:

  1. Convierte tus arrays paralelos en un array de al menos ocho objetos, con id, nombre, precio, categoria, stock y descripcion.
  2. Adapta las funciones del paso 2 a la nueva estructura.
  3. Escribe describir(producto) usando desestructuración en el parámetro.
  4. Añade a algún producto una propiedad anidada y accede a ella con encadenamiento opcional.
  5. Escribe una función que devuelva una copia del catálogo con todos los precios subidos un 5 %, sin tocar el original. Compruébalo imprimiendo los dos.
Consultar · modificar, añadir y recorrer propiedades
producto.stock = 3;              // modificar
producto.descuento = 0.1;        // añadir
delete producto.descuento;       // borrar

Object.keys(producto);           // ["id", "nombre", "precio", ...]
Object.values(producto);
Object.entries(producto);        // pares [clave, valor]

Paso 4 · JSON de ida y vuelta · 20 min

  1. Convierte el catálogo a JSON con sangría y obsérvalo.
  2. Vuelve a convertirlo a objeto y comprueba que todo sigue igual.
  3. Rompe el JSON a propósito de cuatro formas distintas —una comilla simple, una coma final, un comentario y una clave sin comillas— y anota qué dice el error en cada caso.
  4. Añade al catálogo un producto con una propiedad de valor undefined y otra que sea una función. Conviértelo a JSON y mira qué ha pasado con ellas. No se trata de un error, sino de una regla del formato que conviene conocer antes de la UD6.

Paso 5 · Tus funciones frente a los métodos · 20 min

En la sesión 3 escribiste aplicarATodos, filtrarPor, contarSi y, si hiciste la ampliación, reducir. Ponlas al lado de map, filter y reduce.

Tu función Método equivalente ¿Devuelven lo mismo? ¿Qué hace el método que la tuya no?
aplicarATodos map
filtrarPor filter
contarSi
reducir reduce

La tercera fila tiene truco: no hay un método que se llame así. Decide con qué lo escribirías y por qué.

Al terminar, responde en tres líneas: ahora que existen los métodos, ¿para qué ha servido escribir las tuyas a mano?

Ampliación si has completado el trabajo

Primero termina y comprueba los cinco pasos. El primer reto usa reduce para algo que no es sumar; el segundo enseña por qué el catálogo tenía que dejar de ser listas paralelas.

Reto 1 · reduce no sirve solo para sumar

El acumulador de reduce no tiene por qué ser un número: puede ser un array o un objeto, y ahí es donde el método deja de ser una curiosidad.

  1. Agrupa por categoría. Escribe agruparPorCategoria(productos) que devuelva un objeto donde cada clave es una categoría y su valor, el array de productos de esa categoría. Con reduce, en una sola pasada.
  2. Indexa por id. Escribe indexarPorId(productos) que devuelva un objeto donde la clave es el id. Explica por qué eso hace que buscar un producto sea inmediato, y qué pierdes respecto al array.
  3. Cuenta por categoría. Ahora el acumulador es un objeto de números.
  4. Resume en una pasada. Escribe una función que devuelva a la vez el total, el máximo, el mínimo y el recuento, recorriendo la lista una sola vez. El acumulador es un objeto con cuatro campos.
  5. Compara el resultado del apartado 4 con hacer cuatro recorridos separados. ¿Cuál se lee mejor? ¿Cuál harías en un catálogo de ocho productos y cuál en uno de cincuenta mil?

Reto 2 · Las listas que se desincronizan

Este código funcionaba perfectamente hasta que alguien retiró un producto del catálogo.

const nombres  = ["Teclado", "Ratón", "Monitor", "Webcam"];
const precios  = [89.9, 24.5, 199, 69];
const stocks   = [4, 0, 7, 2];

function retirar(indice) {
  nombres.splice(indice, 1);
  precios.splice(indice, 1);
}

retirar(1);

for (let i = 0; i < nombres.length; i++) {
  console.log(`${nombres[i]}: ${precios[i]} € · ${stocks[i]} unidades`);
}
  1. Ejecútalo y anota qué imprime. Ninguna línea da error, y tres de las tres son falsas.
  2. Explica exactamente qué se ha desincronizado y por qué el bucle no puede detectarlo.
  3. Reescríbelo con un array de objetos y comprueba que el mismo retirar ya no puede producir ese fallo. Enuncia por qué en una frase.
  4. Una defensa intermedia: escribe una comprobación que, con las listas paralelas, hubiera avisado del problema al arrancar. ¿Qué tendría que comparar?
  5. Este fallo tiene nombre en la disciplina: los datos de una misma entidad viven en sitios distintos y nada garantiza que se mantengan de acuerdo. Escribe en dos líneas por qué agrupar en un objeto lo elimina de raíz en lugar de detectarlo.
Objetivo mínimoCatálogo de objetos con las funciones adaptadas, consultas con el método adecuado y conversión JSON de ida y vuelta.
Si lo tienesLa copia con precios subidos sin tocar el original, los cuatro JSON rotos diagnosticados y la tabla de comparación con tus funciones.
RetoLas cuatro agrupaciones con reduce en una sola pasada, y las listas paralelas desincronizadas explicadas y corregidas.

Cierre

5 minutos · comprobación y recuerdo

Lista de verificación de la sesión

  • Tu catálogo es un array de objetos, no listas paralelas.
  • Distingues los métodos que mutan de los que devuelven, y copias antes de ordenar.
  • Eliges el método por lo que devuelve, no por costumbre.
  • Pasas función de comparación a sort con números.
  • Sabes copiar un objeto y por qué hace falta.
  • Conviertes a JSON y desde JSON, y sabes qué reglas tiene.

Antes de cerrar · 3 minutos, sin mirar

  1. Nombra dos métodos que muten y dos que no.
  2. ¿Qué hace [...lista]?
  3. ¿Qué devuelve find si no encuentra nada?
  4. ¿Por qué [10, 9, 100].sort() da un orden raro?
  5. ¿Se puede modificar un objeto declarado con const?
  6. ¿Qué tres reglas de JSON son más estrictas que las de JavaScript?
Ver respuestas

1 · Mutan push y sort; no mutan slice y map.

2 · Crea un array nuevo con los mismos elementos: una copia superficial.

3 · undefined, y hay que comprobarlo antes de usar el resultado.

4 · Porque sin comparador ordena como texto, y "100" va antes que "9".

5 · Sí: const impide reasignar la variable, no modificar el objeto al que apunta.

6 · Las claves entre comillas dobles, sin comas finales y sin comentarios. Tampoco admite funciones ni undefined.

Microprueba semanal 4 · 5–10 minutos

Individual, sin IA y sin apuntes.

  1. Di qué método usarías para: los productos caros, el primero caro, saber si hay alguno caro, y el valor total del almacén.
  2. Ordena una lista de números de menor a mayor sin alterar la original.
  3. Escribe el JSON de un producto con tres campos, respetando sus reglas.