El kit funcional: forEach y map
Durante todo el bloque usaremos este dataset — guárdalo, es tu campo de pruebas:
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»:
for (let i = 0; i < pedidos.length; i++) {
console.log(pedidos[i].cliente);
}
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:
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 | map | |
|---|---|---|
| ¿Para qué? | efectos (logear, pintar) | transformar datos |
| ¿Qué devuelve? | undefined | array nuevo igual de largo |
| ¿Toca el original? | NO — ambos son inmutables | |
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:
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
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:
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:
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 }
pedidos.reduce((max, p) =>
p.importe > max ? p.importe : max, 0). Un ternario del Bloque 2 haciendo de
comparador.Detectores: find, some, every y sort
3.1 · Preguntas de sí/no: some y every
pedidos.some((p) =>
p.estado === "cancelado"
);
// true — con que UNO cumpla basta
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:
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(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:
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!
[...pedidos]) crean una copia defensiva ANTES de ordenar. Esa línea es oro:
memorízala.a.cliente.localeCompare(b.cliente)
como comparator: ordena bien ñ, acentos y mayúsculas.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:
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
- filter: entran 5 pedidos, salen los 4 no cancelados.
- map: esos 4 objetos se convierten en 4 strings.
- join: los 4 strings se funden en UN texto final.
4.2 · Inmutabilidad: la norma de la casa
| ❌ 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:
const actualizado = {
...pedidos[0],
estado: "enviado"
};
// pedidos[0] sigue teniendo su estado original ✔
✍️ 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
forEachy transformaciones conmapyfilter. - 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 ✔
📝 Examen del Bloque 5
Necesitas ≥ 70 % (10/14) para superar el bloque.
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