⚡ Curso JavaScript · Nivel N3 Intermedio

Bloque 5: Programación funcional con arrays

Sube el nivel: olvida escribir bucles a mano. Con forEach, map, filter y reduce declaras QUÉ quieres conseguir, no CÓMO recorrerlo — y encadenándolos construyes pipelines de datos como los de una app real. Todo el bloque trabaja sobre un mismo dataset de pedidos, como en un trabajo verdadero.

⏱ ≈ 12 horas 📚 4 lecciones ✍️ 15 ejercicios con datasets reales 📝 Examen final superable (≥ 70 %)
Lección 1 · Teoría

El kit funcional: forEach y map

Durante todo el bloque usaremos este dataset — guárdalo, es tu campo de pruebas:

dataset: pedidos de la tienda
const pedidos = [
  { id: 1, cliente: "Ana", importe: 45.90, estado: "enviado" },
  { id: 2, cliente: "Bo",   importe: 120.00, estado: "pendiente" },
  { id: 3, cliente: "Cy",   importe: 19.99, estado: "enviado" },
  { id: 4, cliente: "Di",   importe: 75.50, estado: "cancelado" },
  { id: 5, cliente: "Eli",  importe: 210.00, estado: "pendiente" }
];

1.1 · forEach: recorrer sin bucle

Ejecuta tu callback UNA VEZ POR ELEMENTO. No devuelve nada — es solo «para cada uno, haz esto»:

imperativo (cómo)
for (let i = 0; i < pedidos.length; i++) {
  console.log(pedidos[i].cliente);
}
declarativo (qué)
pedidos.forEach((pedido) => {
  console.log(pedido.cliente);
});
⚠️ forEach devuelve undefined. Si intentas guardar su resultado en una constante, obtendrás… nada. Para PRODUCIR datos, usa map.

1.2 · map: transformar cada elemento

Devuelve un array NUEVO de la misma longitud, con cada elemento transformado por tu callback:

de pedidos a etiquetas
const clientes = pedidos.map((p) => p.cliente);
// ["Ana", "Bo", "Cy", "Di", "Eli"]

const etiquetas = pedidos.map((p) => `#${p.id} ${p.cliente}`);
// ["#1 Ana", "#2 Bo", ...]
forEach vs map en 10 segundos
forEachmap
¿Para qué?efectos (logear, pintar)transformar datos
¿Qué devuelve?undefinedarray nuevo igual de largo
¿Toca el original?NO — ambos son inmutables
💡 Regla mnemotécnica: si vas a usar el resultado → map; si solo quieres que algo ocurra (imprimir, acumular en pantalla) → forEach.
Lección 2 · Teoría

filter y reduce a fondo

2.1 · filter: quédate con los que cumplen

Tu callback decide con true/false. Los que votan true pasan al array resultado; el original queda intacto:

subconjuntos al instante
const grandes = pedidos.filter((p) => p.importe > 100);
// [Bo 120, Eli 210]

const enviados = pedidos.filter((p) => p.estado === "enviado");
console.log(enviados.length); // 2
⚠️ filter NUNCA devuelve menos elementos de los que cumplen ni modifica el original. Si nadie cumple, obtienes un array VACÍO — no null ni undefined.

2.2 · reduce: resumir todo en un valor

El más potente del kit: camina el array llevando un acumulador. Tu callback recibe (acumulador, elemento) y devuelve el acumulador actualizado:

la factura total
const total = pedidos.reduce((suma, p) => suma + p.importe, 0);
console.log(total.toFixed(2)); // "471.39"

Paso a paso: empieza en 0 (el valor inicial), suma 45.90, luego 120… El segundo argumento de reduce ES ese punto de partida.

2.3 · reduce también construye objetos

«Resumir a un valor» incluye objetos y arrays. Contar por categoría es EL patrón que verás en entrevistas:

contador por estado
const porEstado = pedidos.reduce((conteo, p) => {
  conteo[p.estado] = (conteo[p.estado] || 0) + 1;
  return conteo;
}, {});

console.log(porEstado);
// { enviado: 2, pendiente: 2, cancelado: 1 }
💡 Y el máximo sin ordenar nada: pedidos.reduce((max, p) => p.importe > max ? p.importe : max, 0). Un ternario del Bloque 2 haciendo de comparador.
Lección 3 · Teoría

Detectores: find, some, every y sort

3.1 · Preguntas de sí/no: some y every

some: ¿al menos uno?
pedidos.some((p) =>
  p.estado === "cancelado"
);
// true — con que UNO cumpla basta
every: ¿todos?
pedidos.every((p) =>
  p.importe > 0
);
// true — hay que cumplir TODOS

3.2 · find: el primero que cumple

A diferencia de filter (que devuelve un ARRAY), find devuelve EL ELEMENTO completo, o undefined si no hay ninguno:

búsqueda por id
const buscado = pedidos.find((p) => p.id === 3);
console.log(buscado.cliente); // "Cy"

const fantasma = pedidos.find((p) => p.id === 99);
console.log(fantasma);        // undefined
filter vs find en una línea
filter(f)[0]también funciona… pero find para en el primero: más rápido y más claro

3.3 · sort con criterio profesional

Sin comparator, sort ordena COMO TEXTO (¡100 antes que 25!). Con dataset de objetos, el comparator compara campos:

ranking por importe
const ranking = [...pedidos].sort((a, b) => b.importe - a.importe);

console.log(ranking[0].cliente); // "Eli" — la mayor venta
console.log(pedidos[0].cliente);  // "Ana" — ¡el original sigue igual!
⚠️ sort MUTA. Los tres puntitos del spread ([...pedidos]) crean una copia defensiva ANTES de ordenar. Esa línea es oro: memorízala.
💡 Para textos con acentos usa a.cliente.localeCompare(b.cliente) como comparator: ordena bien ñ, acentos y mayúsculas.
Lección 4 · Teoría

Chaining e inmutabilidad

Aquí está la magia del estilo funcional: como map/filter devuelven arrays nuevos, puedes encadenar otro método detrás. El resultado se va moldeando paso a paso, como una cinta de fábrica:

pipeline: filtrar → transformar → unir
const informe = pedidos
  .filter((p) => p.estado !== "cancelado")
  .map((p) => `${p.cliente} (${p.importe.toFixed(2)} €)`)
  .join(" | ");

console.log(informe);
// "Ana (45.90 €) | Bo (120.00 €) | Cy (19.99 €) | Eli (210.00 €)"

4.1 · Leer un pipeline

  1. filter: entran 5 pedidos, salen los 4 no cancelados.
  2. map: esos 4 objetos se convierten en 4 strings.
  3. join: los 4 strings se funden en UN texto final.
💡 El chaining sustituye bucles anidados. Donde antes escribías un for dentro de otro if, ahora escribes una frase que se lee de arriba abajo.

4.2 · Inmutabilidad: la norma de la casa

Mutar vs transformar
❌ Muta el original✅ Devuelve uno nuevo
arr.sort(), arr.reverse(), arr.splice(), arr.push() [...arr].sort(), [...arr].reverse(), arr.slice(), [...arr, x], map, filter, concat

Y para «modificar» un objeto sin tocarlo: spread + sobrescritura:

nueva versión, misma pieza
const actualizado = {
  ...pedidos[0],
  estado: "enviado"
};
// pedidos[0] sigue teniendo su estado original ✔
🎉 Cuando encadenes filter → map → reduce sin pensar ya habrás cambiado de chip: piensas en TRANSFORMACIONES, no en bucles. El Bloque 6 saca estos datos a la interfaz: DOM, eventos y almacenamiento.
Práctica guiada

✍️ Los 15 ejercicios del bloque

Crea la carpeta js-b5/. Todos los ejercicios parten del dataset pedidos de la Lección 1 — cópialo en cada archivo (ej01.html…). Consola con F12.

Sección A · forEach y map

1 Pasador de pedidos

Con forEach, imprime «Pedido #id → cliente (estado)» por cada pedido.

Ver solución
pedidos.forEach((p) => {
  console.log(`Pedido #${p.id} → ${p.cliente} (${p.estado})`);
});

2 Lista de clientes

Genera con map un array solo con los nombres de cliente y muéstralo. Comprueba que pedidos.length === clientes.length.

Ver solución
const clientes = pedidos.map((p) => p.cliente);

console.log(clientes);
console.log(`¿Misma longitud? ${clientes.length === pedidos.length}`);

3 Precios con IVA

Con map, crea pedidosNuevos: los mismos objetos pero con importe subido un 21 % — usando spread para no tocar el original. Verifica que el importe del primer pedido original no cambió.

Ver solución
const conIva = pedidos.map((p) => ({
  ...p,
  importe: p.importe * 1.21
}));

console.log(conIva[0].importe.toFixed(2)); // "55.54"
console.log(pedidos[0].importe);            // 45.9 ✔ intacto

Sección B · filter y reduce

4 Solo enviados

Filtra los pedidos con estado «enviado» y logea cuántos son y sus ids.

Ver solución
const enviados = pedidos.filter((p) => p.estado === "enviado");

console.log(`Enviados: ${enviados.length}`);
console.log(enviados.map((p) => p.id));

5 Facturación total

Con reduce, calcula el importe sumado de TODOS los pedidos. Muéstralo con dos decimales.

Ver solución
const total = pedidos.reduce((suma, p) => suma + p.importe, 0);

console.log(`Total facturado: ${total.toFixed(2)} €`); // 471.39 €

6 El pedido más grande

Sin ordenar el array: usa reduce para encontrar EL PEDIDO (el objeto entero) de mayor importe.

Ver solución
const top = pedidos.reduce((mejor, p) =>
  p.importe > mejor.importe ? p : mejor
);

console.log(top); // Eli, 210 €

7 Contador por estado

Reproduce el patrón de la Lección 2.3: reduce que devuelve { enviado: n, pendiente: n, cancelado: n }.

Ver solución
const resumen = pedidos.reduce((conteo, p) => {
  conteo[p.estado] = (conteo[p.estado] || 0) + 1;
  return conteo;
}, {});

console.log(resumen); // { enviado: 2, pendiente: 2, cancelado: 1 }

Sección C · Detectores y sort

8 Semáforo del negocio

Responde con some/every: ¿hay algún pedido cancelado? ¿TODOS superan 10 €? Logea ambas respuestas.

Ver solución
const hayCancelados = pedidos.some((p) => p.estado === "cancelado");
const todosGrandes = pedidos.every((p) => p.importe > 100);

console.log(hayCancelados); // true
console.log(todosGrandes);   // false — Cy se queda en 19.99 €

9 Buscador por id

Con find, localiza el pedido con id 4. Si existe, imprime su cliente; si no, imprime «No encontrado». Prueba también con id 99.

Ver solución
const resultado = pedidos.find((p) => p.id === 4);

if (resultado) {
  console.log(resultado.cliente);
} else {
  console.log("No encontrado");
}

10 Podio sin romper nada

Crea podio: copia defensiva de pedidos ordenada por importe descendente. Muestra el top-3 (cliente e importe) y verifica que el array original conserva su orden.

Ver solución
const podio = [...pedidos].sort((a, b) => b.importe - a.importe);

podio.slice(0, 3).forEach((p) => {
  console.log(`🥇 ${p.cliente}: ${p.importe} €`);
});

console.log(`Original intacto: ${pedidos[0].cliente === "Ana"}`);

Sección D · Chaining e inmutabilidad

11 Clientes VIP

En UNA expresión encadenada: nombres de los clientes con importe mayor de 100 €. Resultado esperado: ["Bo", "Eli"].

Ver solución
const vips = pedidos
  .filter((p) => p.importe > 100)
  .map((p) => p.cliente);

console.log(vips);

12 Ticket medio de los pendientes

Encadena filter + reduce para calcular la media de importe de los pedidos «pendiente». Cuidado con dividir entre cero si el filtro devuelve vacío.

Ver solución
const pendientes = pedidos.filter((p) => p.estado === "pendiente");

const media = pendientes.length
  ? pendientes.reduce((suma, p) => suma + p.importe, 0) / pendientes.length
  : 0;

console.log(`Ticket medio: ${media.toFixed(2)} €`); // 165.00 €

13 Informe en una línea

Tres métodos encadenados: de los NO cancelados genera strings «cliente: importe €» y únelos con " · ". Un único console.log final.

Ver solución
const informe = pedidos
  .filter((p) => p.estado !== "cancelado")
  .map((p) => `${p.cliente}: ${p.importe.toFixed(2)} €`)
  .join(" · ");

console.log(informe);

14 Archivar sin destruir

Crea archivados: versión nueva del dataset donde cada pedido lleva una propiedad extra archivado: true — sin modificar ningún objeto original. Comprueba que pedidos[0].archivado es undefined.

Ver solución
const archivados = pedidos.map((p) => ({
  ...p,
  archivado: true
}));

console.log(archivados[0].archivado);   // true
console.log(pedidos[0].archivado);     // undefined ✔ original intacto

15 🏆 Proyecto integrador: panel-de-ventas.html

Crea panel-de-ventas.html. Requisitos mínimos:

  • Tu propio dataset de al menos 5 objetos (ventas con producto, categoría, precio y unidades).
  • Un recorrido con forEach y transformaciones con map y filter.
  • Dos reduce: un total numérico y un resumen tipo objeto.
  • Algunos detectores (some/every/find).
  • Un pipeline encadenado de tres o más métodos que produzca un texto informe.
  • Ordenación SOLO sobre copias defensivas ([...datos].sort(...)).
  • Ninguna mutación del dataset original: compruébalo logeándolo al final.
Ver solución modelo
const ventas = [
  { producto: "Teclado", categoria: "periférico", precio: 25, unidades: 12 },
  { producto: "Monitor", categoria: "pantalla", precio: 199, unidades: 4 },
  { producto: "Ratón", categoria: "periférico", precio: 15, unidades: 30 },
  { producto: "Hub USB", categoria: "accesorio", precio: 19, unidades: 18 },
  { producto: "Webcam", categoria: "vídeo", precio: 49, unidades: 7 }
];

ventas.forEach((v) => console.log(v.producto));

const etiquetas = ventas
  .filter((v) => v.precio < 50)
  .map((v) => v.producto.toUpperCase());

const facturacion = ventas.reduce(
  (total, v) => total + v.precio * v.unidades, 0
);

const porCategoria = ventas.reduce((conteo, v) => {
  conteo[v.categoria] = (conteo[v.categoria] || 0) + v.unidades;
  return conteo;
}, {});

console.log(ventas.some((v) => v.precio > 150));
console.log(ventas.find((v) => v.categoria === "vídeo"));

const ranking = [...ventas]
  .sort((a, b) => b.precio - a.precio)
  .map((v) => `${v.producto} (${v.precio} €)`)
  .join(" | ");

console.log(etiquetas, facturacion, porCategoria);
console.log(ranking);
console.log(ventas); // orden original intacto ✔
Evaluación

📝 Examen del Bloque 5

Necesitas ≥ 70 % (10/14) para superar el bloque.

1 · forEach()…
2 · map() siempre devuelve…
3 · Si NINGÚN elemento cumple la condición de filter, obtienes…
4 · reduce sirve para…
5 · En reduce((acc, x) => acc + x, 0), el 0 final es…
6 · some() devuelve true si…
7 · every() exige que…
8 · La diferencia clave entre find y filter…
9 · [100, 25, 4].sort() SIN comparator produce…
10 · Para ordenar sin mutar el original escribo…
11 · El encadenamiento (chaining) es posible porque…
12 · ¿Cuál es estilo DECLARATIVO?
13 · Si guardas const r = arr.forEach(…), r vale…
14 · Un reduce puede construir…

Parte B · Reto práctico final

Construye el ejercicio 15 («panel-de-ventas.html») sin mirar soluciones y valida tú mismo/a estos criterios. Mínimo para aprobar: 7 de 8 casillas.

Cumplidos: 0 / 8

🎉 ¿Superaste las dos partes? Enhorabuena: has aprobado el Bloque 5 de JavaScript. Ya procesas datos como un profesional: pipelines declarativos, inmutabilidad y resúmenes con reduce. En el Bloque 6 todo esto salta a la pantalla: DOM, eventos y almacenamiento.