Al cerrar la lección anterior quedó una lista de tareas pendientes muy concreta: buscar la primera tarea que cumple una condición te costó una función entera, agrupar por estado te costó un bucle dentro de otro, el resumen te costó tres variables acumuladoras y ordenar el tablero por fecha límite ni siquiera sabías hacerlo. Todas esas operaciones son tan universales que JavaScript las trae resueltas y con nombre propio. En esta lección aprenderás el arsenal completo para consultar una lista: find, findIndex, findLast, some, every y filter para buscar y seleccionar; sort para ordenar —con la trampa más famosa del lenguaje, que hace que [10, 9, 100] se ordene mal—; y reduce, la herramienta más potente y la peor explicada de todas, que veremos despacio y en cuatro casos reales. Al final montarás el informe semanal de carga de Taller Nómada, que es el entregable que Marta lleva pidiendo desde el Módulo 1.
Contenido
- El punto de partida: el backlog canónico
- Buscar un elemento:
find,findIndex,findLast - Comprobar el conjunto:
someyevery - Seleccionar varios:
filter - La tabla de la búsqueda
sort: la trampa de los números- La función comparadora
- Ordenar por número, por texto y por fecha
- Orden descendente y ordenación por varios criterios
toSortedytoReversed: ordenar sin mutarreduce, despacio- Cuatro casos reales de
reduce Object.groupBy,MapySet- Encadenar
filter().map().sort()y su coste - Ejemplo integrador: el informe semanal de carga
- Errores Comunes y Consejos
- Ejercicios
- Conclusión
- El punto de partida: el backlog canónico
Todo lo que sigue trabaja sobre el mismo backlog de 04-01. Lo repetimos en versión compacta para tenerlo a mano:
'use strict';
const HOY = '2026-09-20';
const PESOS = { alta: 3, media: 2, baja: 1 };
const backlog = [
{ id: 1, titulo: 'Rediseñar la sala polivalente', responsable: 'Iván', prioridad: 'alta', estado: 'en-curso', etiquetas: ['espacio', 'diseño'], horasEstimadas: 12, fechaLimite: '2026-09-30', revisor: 'Marta' },
{ id: 2, titulo: 'Cartelería del taller de serigrafía', responsable: 'Marta', prioridad: 'media', estado: 'pendiente', etiquetas: ['serigrafía', 'comunicación'], horasEstimadas: 6, fechaLimite: '2026-10-15', revisor: null },
{ id: 3, titulo: 'Actualizar la web de reservas', responsable: 'Lucía', prioridad: 'alta', estado: 'pendiente', etiquetas: ['web', 'reservas'], horasEstimadas: 14, fechaLimite: '2026-10-02', revisor: 'Iván' },
{ id: 4, titulo: 'Inventario de tintas de serigrafía', responsable: 'Marta', prioridad: 'baja', estado: 'hecha', etiquetas: ['serigrafía', 'almacén'], horasEstimadas: 3, fechaLimite: '2026-09-12', revisor: null },
{ id: 5, titulo: 'Guía de encuadernación para residentes', responsable: 'Iván', prioridad: 'media', estado: 'en-curso', etiquetas: ['encuadernación', 'documentación'], horasEstimadas: 8, fechaLimite: '2026-11-05', revisor: 'Lucía' },
{ id: 6, titulo: 'Presupuesto de la carpintería', responsable: 'Iván', prioridad: 'alta', estado: 'pendiente', etiquetas: ['carpintería', 'compras'], horasEstimadas: 5, fechaLimite: '2026-09-05', revisor: 'Marta' }
];Todos los métodos de esta lección reciben una función como argumento: son funciones de orden superior, exactamente las que estudiaste en 03-06. La función que devuelve true o false para decidir si un elemento «vale» se llama predicado.
- Buscar un elemento:
find, findIndex, findLast
find, findIndex, findLastfind(predicado) devuelve el primer elemento que cumple la condición, o undefined si no hay ninguno. Y se detiene en cuanto lo encuentra.
const sobrecargada = backlog.find((t) => t.horasEstimadas > 10);
console.log(sobrecargada.titulo); // 'Rediseñar la sala polivalente'
const inexistente = backlog.find((t) => t.horasEstimadas > 100);
console.log(inexistente); // undefinedCompáralo con la función que escribiste a mano en el ejercicio 2 de la lección anterior:
// Antes: siete líneas
function primeraTareaSobrecargada(tareas, limite) {
for (const tarea of tareas) {
if (tarea.horasEstimadas > limite) return tarea;
}
return null;
}
// Ahora: una
const primera = backlog.find((t) => t.horasEstimadas > 10);La única diferencia de comportamiento es que find devuelve undefined y tu función devolvía null. Como el resultado puede no existir, protégete siempre con ?. o ??:
console.log(backlog.find((t) => t.responsable === 'Nadie')?.titulo ?? 'sin resultados');
// 'sin resultados'findIndex(predicado) devuelve la posición en lugar del elemento, o -1:
console.log(backlog.findIndex((t) => t.id === 6)); // 5
console.log(backlog.findIndex((t) => t.id === 99)); // -1Es lo que necesitas cuando vas a modificar el array por posición, por ejemplo con splice:
function eliminarPorId(tareas, id) {
const posicion = tareas.findIndex((t) => t.id === id);
if (posicion === -1) return null;
const [eliminada] = tareas.splice(posicion, 1);
return eliminada;
}findLast y findLastIndex hacen lo mismo recorriendo desde el final:
console.log(backlog.find((t) => t.prioridad === 'alta').titulo);
// 'Rediseñar la sala polivalente' ← la primera alta
console.log(backlog.findLast((t) => t.prioridad === 'alta').titulo);
// 'Presupuesto de la carpintería' ← la última altaRecuerda además la limitación de includes e indexOf que viste en 04-03: comparan con === y por eso no sirven con objetos. find sí, porque tú escribes la comparación:
console.log(backlog.includes({ id: 6 })); // false ✗
console.log(backlog.find((t) => t.id === 6) !== undefined); // true ✓
- Comprobar el conjunto:
some y every
some y everyA veces no quieres el elemento, sino una respuesta de sí o no sobre toda la lista.
// some: ¿hay AL MENOS UNO que cumpla?
const hayVencidas = backlog.some((t) => t.fechaLimite < HOY && t.estado !== 'hecha');
console.log(hayVencidas); // true (la 6, «Presupuesto de la carpintería»)
// every: ¿lo cumplen TODOS?
const todasTienenResponsable = backlog.every((t) => typeof t.responsable === 'string');
console.log(todasTienenResponsable); // true
const todasRevisadas = backlog.every((t) => t.revisor !== null);
console.log(todasRevisadas); // false (las tareas 2 y 4 no tienen revisor)Ambas hacen cortocircuito, igual que los operadores && y || de 01-06: some para en el primer true, every para en el primer false. Eso las convierte en la forma correcta —y la más rápida— de validar.
Dos comportamientos con el array vacío que sorprenden y conviene conocer:
console.log([].some((t) => true)); // false ← no hay ninguno que cumpla
console.log([].every((t) => false)); // true ← "todos" los cero elementos cumplenEse true se llama verdad vacua y es matemáticamente correcto («todos los unicornios de esta lista son azules» es verdad si la lista está vacía). En código, tenlo presente: every sobre una lista vacía nunca avisa de nada.
Aplicación al proyecto, validando el backlog antes de guardarlo:
function backlogValido(tareas) {
return tareas.every((t) =>
typeof t.id === 'number' &&
typeof t.titulo === 'string' && t.titulo.trim() !== '' &&
t.horasEstimadas > 0 && t.horasEstimadas <= 40 && // R3
Array.isArray(t.etiquetas)
);
}
console.log(backlogValido(backlog)); // true
console.log(backlogValido([{ id: 9, titulo: '', horasEstimadas: 0, etiquetas: [] }])); // false
- Seleccionar varios:
filter
filterfilter(predicado) devuelve un array nuevo con todos los elementos que cumplen la condición. No muta el original y, a diferencia de map, la longitud del resultado sí cambia.
const abiertas = backlog.filter((t) => t.estado !== 'hecha');
console.log(abiertas.length); // 5
const altasAbiertas = backlog.filter((t) => t.prioridad === 'alta' && t.estado !== 'hecha');
console.log(altasAbiertas.map((t) => t.id)); // [ 1, 3, 6 ]
const deIvan = backlog.filter((t) => t.responsable === 'Iván');
console.log(deIvan.length); // 3
const vacio = backlog.filter((t) => t.horasEstimadas > 100);
console.log(vacio); // [] ← array vacío, no undefinedEse último detalle es importante: filter nunca devuelve undefined, siempre un array (posiblemente vacío). Por eso filter(...).length === 0 es la comprobación correcta, y no if (resultado), que sería siempre verdadero porque un array vacío es truthy (01-07).
Diferencias con find, que se confunden a menudo:
find |
filter |
|
|---|---|---|
| Devuelve | Un elemento o undefined |
Un array (posiblemente vacío) |
| ¿Recorre entero? | No: para en el primero | Sí, siempre |
| Cuándo usarlo | Buscas uno concreto (por id) |
Quieres todos los que cumplan |
Un error frecuente es usar filter cuando querías find:
// ✗ Recorre las seis tareas y devuelve un array de un elemento
const tarea = backlog.filter((t) => t.id === 3)[0];
// ✓ Para en cuanto la encuentra y devuelve la tarea
const tarea2 = backlog.find((t) => t.id === 3);
- La tabla de la búsqueda
| Método | Qué devuelve | ¿Para pronto? | Úsalo cuando… |
|---|---|---|---|
find(p) |
El primer elemento que cumple, o undefined |
Sí | Buscas una tarea concreta |
findIndex(p) |
Su posición, o -1 |
Sí | Vas a modificar el array por índice |
findLast(p) |
El último que cumple, o undefined |
Sí (desde el final) | Te interesa el más reciente |
findLastIndex(p) |
Su posición, o -1 |
Sí (desde el final) | Igual, pero necesitas la posición |
some(p) |
true / false |
Sí, en el primer true |
«¿Hay alguna vencida?» |
every(p) |
true / false |
Sí, en el primer false |
«¿Son todas válidas?» |
filter(p) |
Array con todos los que cumplen | No | Quieres una sublista |
includes(v) |
true / false |
Sí | Buscas un primitivo exacto |
indexOf(v) |
Posición o -1 |
Sí | Buscas la posición de un primitivo |
sort: la trampa de los números
sort: la trampa de los númerosEmpecemos por el desastre, porque es la mejor forma de que no se te olvide:
const numeros = [10, 9, 100, 1, 25];
numeros.sort();
console.log(numeros); // [ 1, 10, 100, 25, 9 ] ✗ ¿¿qué??No es un error del motor. sort() sin argumentos convierte cada elemento a string y los compara alfabéticamente, carácter a carácter. Como '1' va antes que '2' y '2' antes que '9', el resultado es un orden de diccionario: '1', '10', '100', '25', '9'.
Y hay una segunda trampa igual de importante: sort muta el array original, como ya anunciaba la tabla de 04-03.
const horas = [12, 6, 14, 3, 8, 5];
const ordenadas = horas.sort();
console.log(horas === ordenadas); // true ← es el MISMO array, no una copia
console.log(horas); // [ 12, 14, 3, 5, 6, 8 ] ← el original, destrozadoDos reglas que se derivan de esto y que debes aplicar siempre:
- A
sortse le pasa siempre una función comparadora. Sin excepciones, ni siquiera con números. - Copia antes de ordenar, salvo que quieras mutar de verdad:
array.slice().sort(cmp)o, mejor,array.toSorted(cmp).
- La función comparadora
La comparadora recibe dos elementos y devuelve un número que responde a la pregunta «¿cuál va antes?»:
| Devuelve | Significado |
|---|---|
| Un número negativo | a va antes que b |
| Cero | Da igual el orden entre ellos |
| Un número positivo | a va después que b |
const horas = [12, 6, 14, 3, 8, 5];
// Ascendente: si a - b es negativo, a es menor y va antes
console.log(horas.toSorted((a, b) => a - b)); // [ 3, 5, 6, 8, 12, 14 ]
// Descendente: se invierte la resta
console.log(horas.toSorted((a, b) => b - a)); // [ 14, 12, 8, 6, 5, 3 ]El truco a - b funciona porque la resta ya produce el signo correcto: si a es menor, la resta es negativa. Solo sirve con números; con strings hay que comparar de otra forma (apartado 8).
Una comparadora escrita a la larga, para ver qué ocurre por dentro:
function porHorasAscendente(a, b) {
if (a.horasEstimadas < b.horasEstimadas) return -1;
if (a.horasEstimadas > b.horasEstimadas) return 1;
return 0;
}Y un aviso: la comparadora debe ser coherente. Si dices que A va antes que B y también que B va antes que A, el resultado es imprevisible. Nunca uses Math.random() dentro de una comparadora para «barajar»: puede producir órdenes sesgados o incluso errores.
- Ordenar por número, por texto y por fecha
Por número (el caso de horasEstimadas y id):
const porHoras = backlog.toSorted((a, b) => a.horasEstimadas - b.horasEstimadas);
console.log(porHoras.map((t) => `${t.horasEstimadas}h`).join(' '));
// 3h 5h 6h 8h 12h 14hPor texto. La resta no sirve con strings ('a' - 'b' es NaN). Y el operador < compara por código de carácter, lo que coloca mal las mayúsculas y los acentos. La respuesta correcta es localeCompare, que compara según las reglas del idioma:
console.log('á'.localeCompare('b', 'es')); // -1 ← 'á' va antes que 'b' en español
console.log('á' < 'b'); // false ✗ comparación por código
const porTitulo = backlog.toSorted((a, b) => a.titulo.localeCompare(b.titulo, 'es'));
console.log(porTitulo.map((t) => t.id)); // [ 3, 2, 5, 4, 6, 1 ]
console.log(porTitulo[0].titulo); // 'Actualizar la web de reservas'
console.log(porTitulo[2].titulo); // 'Guía de encuadernación para residentes'localeCompare devuelve un número negativo, cero o positivo: exactamente lo que necesita sort. El segundo argumento es el idioma; el tercero admite opciones como { sensitivity: 'base' } para ignorar mayúsculas y acentos:
const nombres = ['Iván', 'ana', 'Álvaro', 'Bruno'];
console.log(nombres.toSorted((a, b) => a.localeCompare(b, 'es', { sensitivity: 'base' })));
// [ 'Álvaro', 'ana', 'Bruno', 'Iván' ]Por fecha. Aquí se recoge el fruto de una decisión de diseño del Módulo 1: fechaLimite es un string ISO 'yyyy-mm-dd', y ese formato se ordena alfabéticamente igual que cronológicamente. Por eso basta con comparar strings:
const porFecha = backlog.toSorted((a, b) => a.fechaLimite.localeCompare(b.fechaLimite));
console.log(porFecha.map((t) => `${t.fechaLimite} (${t.id})`).join('\n'));
// 2026-09-05 (6)
// 2026-09-12 (4)
// 2026-09-30 (1)
// 2026-10-02 (3)
// 2026-10-15 (2)
// 2026-11-05 (5)Con el formato '30/09/2026' esto sería imposible sin convertir antes cada fecha. Ese es exactamente el motivo por el que el modelo eligió ISO.
- Orden descendente y ordenación por varios criterios
Para invertir el orden, invierte la comparadora (no ordenes y luego apliques reverse, que es dos pasadas para nada):
const masUrgentesPrimero = backlog.toSorted((a, b) => b.horasEstimadas - a.horasEstimadas);
console.log(masUrgentesPrimero[0].horasEstimadas); // 14Cuando hay empates, se pasa al criterio siguiente. La técnica es evaluar el primer criterio y, si da 0, devolver el segundo:
const ORDEN_PRIORIDAD = { alta: 0, media: 1, baja: 2 };
function porPrioridadYFecha(a, b) {
const porPrioridad = ORDEN_PRIORIDAD[a.prioridad] - ORDEN_PRIORIDAD[b.prioridad];
if (porPrioridad !== 0) return porPrioridad;
return a.fechaLimite.localeCompare(b.fechaLimite); // desempate por fecha
}
const tablero = backlog.toSorted(porPrioridadYFecha);
console.log(tablero.map((t) => `${t.prioridad} ${t.fechaLimite} [${t.id}]`).join('\n'));
// alta 2026-09-05 [6]
// alta 2026-09-30 [1]
// alta 2026-10-02 [3]
// media 2026-10-15 [2]
// media 2026-11-05 [5]
// baja 2026-09-12 [4]Fíjate en el objeto ORDEN_PRIORIDAD: es el mismo patrón de diccionario de consulta de 04-01, aquí usado para convertir un texto ('alta') en un número ordenable. Sin él, 'alta' < 'baja' < 'media' daría el orden alfabético, que no es el que quiere Marta.
Con más de dos criterios, el patrón se generaliza encadenando con ||, aprovechando que 0 es falsy:
const porTresCriterios = (a, b) =>
(ORDEN_PRIORIDAD[a.prioridad] - ORDEN_PRIORIDAD[b.prioridad]) ||
a.fechaLimite.localeCompare(b.fechaLimite) ||
a.titulo.localeCompare(b.titulo, 'es');Se lee así: «usa el primer criterio; si da 0, prueba el segundo; si también da 0, el tercero». Es una aplicación directa del cortocircuito de || que estudiaste en 01-06.
Un último detalle: desde ES2019, sort es estable, lo que significa que los elementos que empatan conservan su orden original. Por eso ordenar primero por fecha y después por prioridad también produce un resultado correcto, aunque el criterio combinado es más claro.
toSorted y toReversed: ordenar sin mutar
toSorted y toReversed: ordenar sin mutarLos has estado usando en todo el apartado anterior. Son las versiones inmutables de sort y reverse, disponibles en JavaScript moderno:
const original = [12, 6, 14];
const ordenado = original.toSorted((a, b) => a - b);
console.log(ordenado); // [ 6, 12, 14 ]
console.log(original); // [ 12, 6, 14 ] ✓ intacto
const alReves = original.toReversed();
console.log(alReves); // [ 14, 6, 12 ]
console.log(original); // [ 12, 6, 14 ] ✓ intacto| Mutante | Inmutable | Qué hace |
|---|---|---|
sort(cmp) |
toSorted(cmp) |
Ordena |
reverse() |
toReversed() |
Invierte |
splice(i, n, v) |
toSpliced(i, n, v) |
Borra/inserta |
arr[i] = v |
with(i, v) |
Reemplaza una posición |
Si trabajas en un entorno antiguo que no los tenga, el equivalente es copiar antes: array.slice().sort(cmp). Lo que nunca debes hacer es ordenar directamente el backlog para mostrarlo de una forma concreta: la lista de datos y la lista de presentación son cosas distintas.
// ✗ Ordena el backlog de verdad; el resto de la aplicación ve otro orden
backlog.sort((a, b) => a.horasEstimadas - b.horasEstimadas);
// ✓ Produce una vista ordenada, sin tocar los datos
const vista = backlog.toSorted((a, b) => a.horasEstimadas - b.horasEstimadas);
reduce, despacio
reduce, despacioreduce es el método más potente del arsenal y el que peor se explica. La idea, en una frase:
reducerecorre el array llevando un valor acumulado, y devuelve ese valor al final.
Ni más ni menos. Es exactamente el patrón acumulador que aprendiste en 02-02, empaquetado en un método. Su firma tiene dos partes:
array.reduce(funcionReductora, valorInicial)
// ↑ (acumulado, elemento, indice, array) => nuevoAcumuladoLa versión más sencilla, sumar horas, comparada con el bucle equivalente:
// Con bucle (lo que sabías hacer)
let total = 0;
for (const tarea of backlog) {
total += tarea.horasEstimadas;
}
console.log(total); // 48
// Con reduce (lo mismo, comprimido)
const total2 = backlog.reduce((acc, tarea) => acc + tarea.horasEstimadas, 0);
console.log(total2); // 48La correspondencia pieza por pieza:
| En el bucle | En reduce |
|---|---|
let total = 0 |
El valor inicial, el segundo argumento |
total += ... |
Lo que devuelve la función reductora |
La variable total al final |
El valor de retorno de reduce |
Sigamos la ejecución vuelta a vuelta, que es la única forma de que se entienda de verdad:
| Vuelta | acc entra |
tarea.horasEstimadas |
Devuelve |
|---|---|---|---|
| 1 | 0 (inicial) | 12 | 12 |
| 2 | 12 | 6 | 18 |
| 3 | 18 | 14 | 32 |
| 4 | 32 | 3 | 35 |
| 5 | 35 | 8 | 43 |
| 6 | 43 | 5 | 48 |
flowchart LR
I["inicial: 0"] --> A["+12 → 12"] --> B["+6 → 18"] --> C["+14 → 32"]
C --> D["+3 → 35"] --> E["+8 → 43"] --> F["+5 → 48"]
Por qué el valor inicial importa tanto. Si lo omites, reduce usa el primer elemento como acumulador inicial y empieza a recorrer desde el segundo. Con números sueltos eso todavía funciona; con objetos, no:
console.log([12, 6, 14].reduce((a, b) => a + b)); // 32 ✓ funciona por casualidad
// ✗ Sin inicial, acc es el objeto tarea completo en la primera vuelta
console.log(backlog.reduce((acc, t) => acc + t.horasEstimadas));
// '[object Object]6143850' ← concatenación de strings, no una sumaY hay un caso todavía peor: sin valor inicial, un array vacío lanza un error.
console.log([].reduce((a, b) => a + b)); // ✗ TypeError: Reduce of empty array with no initial value
console.log([].reduce((a, b) => a + b, 0)); // ✓ 0Regla sin excepciones: pon siempre el valor inicial. Y elígelo con cuidado:
0para sumar,1para multiplicar,''para concatenar,[]para construir una lista,{}para construir un objeto.
El otro error clásico, ya avisado en 03-06, es olvidar devolver el acumulador:
// ✗ La función no devuelve nada: acc pasa a ser undefined en la vuelta 2
const mal = backlog.reduce((acc, t) => { acc.push(t.titulo); }, []);
// ✗ TypeError: Cannot read properties of undefined (reading 'push')
// ✓ Devolver siempre el acumulador
const bien = backlog.reduce((acc, t) => { acc.push(t.titulo); return acc; }, []);
- Cuatro casos reales de
reduce
reduceCaso 1: sumar con condición y peso. El esfuerzo ponderado del backlog, el número 124 que arrastras desde el Módulo 3:
const esfuerzo = backlog.reduce((acc, t) => acc + PESOS[t.prioridad] * t.horasEstimadas, 0);
console.log(esfuerzo); // 124
// Y las horas abiertas, filtrando dentro del propio reduce
const horasAbiertas = backlog.reduce(
(acc, t) => (t.estado === 'hecha' ? acc : acc + t.horasEstimadas),
0
);
console.log(horasAbiertas); // 45Caso 2: contar por estado. El acumulador es un objeto:
const porEstado = backlog.reduce((acc, t) => {
acc[t.estado] = (acc[t.estado] ?? 0) + 1;
return acc;
}, {});
console.log(porEstado); // { 'en-curso': 2, pendiente: 3, hecha: 1 }Dos detalles del código: acc[t.estado] usa corchetes porque la clave está en una variable (04-01), y (acc[...] ?? 0) resuelve la primera aparición, cuando la clave todavía no existe y devolvería undefined.
Caso 3: agrupar tareas por responsable. El acumulador es un objeto cuyos valores son arrays:
const porResponsable = backlog.reduce((acc, t) => {
if (!acc[t.responsable]) acc[t.responsable] = [];
acc[t.responsable].push(t);
return acc;
}, {});
console.log(Object.keys(porResponsable)); // [ 'Iván', 'Marta', 'Lucía' ]
console.log(porResponsable['Iván'].length); // 3
console.log(porResponsable['Iván'].map((t) => t.id)); // [ 1, 5, 6 ]
console.log(porResponsable['Lucía'][0].titulo); // 'Actualizar la web de reservas'Y la variante que interesa a Marta, agrupar sumando horas en lugar de acumular tareas:
const horasPorPersona = backlog.reduce((acc, t) => {
if (t.estado === 'hecha') return acc;
acc[t.responsable] = (acc[t.responsable] ?? 0) + t.horasEstimadas;
return acc;
}, {});
console.log(horasPorPersona); // { Iván: 25, Marta: 6, Lucía: 14 }25 + 6 + 14 = 45, las horas abiertas del backlog. Es exactamente lo que hiciste a mano con bucles en el ejercicio 3 de 04-02, ahora en cinco líneas.
Caso 4: construir un índice por id. El diccionario que en 04-01 montaste con un for:
const indice = backlog.reduce((acc, t) => {
acc[t.id] = t;
return acc;
}, {});
console.log(indice[3].titulo); // 'Actualizar la web de reservas'
console.log(indice[99]?.titulo); // undefined¿Por qué molestarse? Porque backlog.find((t) => t.id === 3) recorre la lista cada vez que buscas, mientras que indice[3] es un acceso directo. Con seis tareas da igual; en el Módulo 9 verás cuándo empieza a importar.
Una advertencia de estilo, que también hicimos en 03-06 con las funciones de orden superior: reduce no es siempre la mejor opción. Si lo que escribes con reduce es más difícil de leer que el bucle equivalente, escribe el bucle. reduce brilla en sumas, conteos, agrupaciones e índices; se vuelve ilegible cuando alguien lo usa para hacer lo que hace map o filter.
Object.groupBy, Map y Set
Object.groupBy, Map y SetObject.groupBy (JavaScript moderno) hace exactamente el caso 3 sin reduce:
const porPrioridad = Object.groupBy(backlog, (t) => t.prioridad);
console.log(Object.keys(porPrioridad)); // [ 'alta', 'media', 'baja' ]
console.log(porPrioridad.alta.map((t) => t.id)); // [ 1, 3, 6 ]
console.log(porPrioridad.media.length); // 2
console.log(porPrioridad.baja[0].titulo); // 'Inventario de tintas de serigrafía'Es más legible que el reduce equivalente, pero es reciente: comprueba el soporte de tu entorno antes de usarlo en producción (en el Módulo 8 verás cómo se comprueban estas cosas). También existe Map.groupBy, que devuelve un Map en lugar de un objeto.
Set es una colección sin duplicados. Es la respuesta a la pregunta que dejamos abierta en 04-03: cómo deduplicar las etiquetas del backlog.
const todas = backlog.flatMap((t) => t.etiquetas);
console.log(todas.length); // 12
const unicas = new Set(todas);
console.log(unicas.size); // 11 ← 'serigrafía' estaba dos veces
console.log(unicas.has('serigrafía')); // true
console.log(unicas.has('marketing')); // false
// Convertirlo de vuelta a array (con Array.from, de 04-03)
const listaUnica = Array.from(unicas).toSorted((a, b) => a.localeCompare(b, 'es'));
console.log(listaUnica);
// [ 'almacén', 'carpintería', 'comunicación', 'compras', 'diseño', 'documentación',
// 'encuadernación', 'espacio', 'reservas', 'serigrafía', 'web' ]El idioma Array.from(new Set(array)) es la forma estándar de eliminar duplicados en JavaScript, y merece la pena memorizarlo.
Map es un diccionario cuyas claves pueden ser de cualquier tipo, conserva el orden de inserción y sabe cuántas entradas tiene:
const conteo = new Map();
for (const t of backlog) {
conteo.set(t.responsable, (conteo.get(t.responsable) ?? 0) + 1);
}
console.log(conteo.get('Iván')); // 3
console.log(conteo.size); // 3
console.log(Array.from(conteo.entries()));
// [ [ 'Iván', 3 ], [ 'Marta', 2 ], [ 'Lucía', 1 ] ]Cuándo elegir cada estructura:
| Necesitas | Usa |
|---|---|
| Agrupar campos con nombre de una entidad | Objeto literal |
| Diccionario con claves de texto conocidas | Objeto literal |
| Diccionario con claves dinámicas, o llevar la cuenta | Map |
| Claves que no son strings (números de verdad, objetos) | Map |
| Una colección sin duplicados | Set |
- Encadenar
filter().map().sort() y su coste
filter().map().sort() y su costeComo filter, map, toSorted y flatMap devuelven arrays, se pueden encadenar, y el resultado se lee casi como una frase:
const informeAltas = backlog
.filter((t) => t.prioridad === 'alta' && t.estado !== 'hecha')
.toSorted((a, b) => a.fechaLimite.localeCompare(b.fechaLimite))
.map((t) => `${t.fechaLimite} · ${t.titulo} (${t.responsable}, ${t.horasEstimadas} h)`);
console.log(informeAltas.join('\n'));
// 2026-09-05 · Presupuesto de la carpintería (Iván, 5 h)
// 2026-09-30 · Rediseñar la sala polivalente (Iván, 12 h)
// 2026-10-02 · Actualizar la web de reservas (Lucía, 14 h)Léelo de arriba abajo: quédate con las de prioridad alta abiertas, ordénalas por fecha, conviértelas en texto. Cada paso tiene un nombre y se puede leer por separado, que es justo lo que buscabas en 03-06 con filtrar → transformar → agregar.
El coste: cada eslabón de la cadena recorre el array entero y crea uno nuevo. Tres métodos encadenados son tres recorridos y tres arrays intermedios, mientras que un for haría una sola pasada sin crear nada.
| Cadena de tres métodos | Bucle único | |
|---|---|---|
| Recorridos | 3 | 1 |
| Arrays intermedios | 2 | 0 |
| Legibilidad | Muy alta | Media |
Con seis tareas, la diferencia es literalmente inapreciable. Con cientos de miles de elementos empieza a notarse, y ahí entran las técnicas de Optimización del Rendimiento. El consejo, hoy: encadena y prioriza la legibilidad, y optimiza solo cuando midas que hace falta. Un detalle gratuito: pon el filter primero en la cadena, para que los pasos siguientes trabajen sobre menos elementos.
- Ejemplo integrador: el informe semanal de carga
Marta abre el panel cada lunes y quiere una foto completa del backlog. Con lo aprendido, cabe en una función.
'use strict';
function informeSemanal(tareas, hoy) {
const abiertas = tareas.filter((t) => t.estado !== 'hecha');
const totales = {
tareas: tareas.length,
abiertas: abiertas.length,
horasTotales: tareas.reduce((acc, t) => acc + t.horasEstimadas, 0),
horasAbiertas: abiertas.reduce((acc, t) => acc + t.horasEstimadas, 0),
esfuerzo: tareas.reduce((acc, t) => acc + PESOS[t.prioridad] * t.horasEstimadas, 0)
};
const carga = abiertas.reduce((acc, t) => {
acc[t.responsable] = (acc[t.responsable] ?? 0) + t.horasEstimadas;
return acc;
}, {});
const vencidas = abiertas
.filter((t) => t.fechaLimite < hoy)
.toSorted((a, b) => a.fechaLimite.localeCompare(b.fechaLimite));
const proximas = abiertas
.toSorted((a, b) => a.fechaLimite.localeCompare(b.fechaLimite))
.slice(0, 3);
const etiquetas = Array.from(new Set(tareas.flatMap((t) => t.etiquetas)))
.toSorted((a, b) => a.localeCompare(b, 'es'));
return { totales, carga, vencidas, proximas, etiquetas, hayRiesgo: vencidas.length > 0 };
}
function imprimirInforme(informe, hoy) {
const { totales, carga, vencidas, proximas, etiquetas } = informe;
console.log(`═══ Informe semanal · Taller Nómada · ${hoy} ═══`);
console.log(`${totales.abiertas} de ${totales.tareas} tareas abiertas · ` +
`${totales.horasAbiertas} h de ${totales.horasTotales} h · ` +
`esfuerzo ponderado ${totales.esfuerzo}`);
console.log('\nCarga por persona:');
Object.entries(carga)
.toSorted((a, b) => b[1] - a[1])
.forEach((par) => console.log(` ${par[0].padEnd(6)} ${par[1]} h`));
console.log(`\nVencidas (${vencidas.length}):`);
if (vencidas.length === 0) console.log(' — ninguna —');
vencidas.forEach((t) => console.log(` ⚠ ${t.fechaLimite} ${t.titulo} (${t.responsable})`));
console.log('\nPróximos vencimientos:');
proximas.forEach((t) => console.log(` ${t.fechaLimite} ${t.titulo}`));
console.log(`\nEtiquetas en uso (${etiquetas.length}): ${etiquetas.join(', ')}`);
}
imprimirInforme(informeSemanal(backlog, HOY), HOY);Salida:
═══ Informe semanal · Taller Nómada · 2026-09-20 ═══ 5 de 6 tareas abiertas · 45 h de 48 h · esfuerzo ponderado 124 Carga por persona: Iván 25 h Lucía 14 h Marta 6 h Vencidas (1): ⚠ 2026-09-05 Presupuesto de la carpintería (Iván) Próximos vencimientos: 2026-09-05 Presupuesto de la carpintería 2026-09-30 Rediseñar la sala polivalente 2026-10-02 Actualizar la web de reservas Etiquetas en uso (11): almacén, carpintería, comunicación, compras, diseño, documentación, encuadernación, espacio, reservas, serigrafía, web
Repasa qué método resuelve cada bloque: filter para las abiertas y las vencidas, reduce para los tres totales y para la carga por persona, toSorted con localeCompare para el orden por fecha y por texto, slice(0, 3) para quedarse con los tres próximos vencimientos, flatMap + Set para las etiquetas únicas y Object.entries + toSorted para ordenar el objeto de carga por horas descendentes. Compáralo con el motor de informes que montaste en 03-06 sobre arrays paralelos: la lógica es la misma, pero cabe en una pantalla y se lee.
(La línea const { totales, carga, ... } = informe; es desestructuración de objetos, y es el tema de 04-07. Léela por ahora como «saca estas propiedades a variables sueltas».)
Errores Comunes y Consejos
1. Llamar a sort() sin comparadora. Ordena como texto: [10, 9, 100] se convierte en [10, 100, 9]. Pasa siempre (a, b) => a - b o la comparadora que toque.
2. Olvidar que sort y reverse mutan. Usa toSorted/toReversed, o copia con slice() antes.
3. Omitir el valor inicial de reduce. Con objetos produce basura y con arrays vacíos lanza TypeError.
4. Olvidar return en la función reductora. El acumulador pasa a undefined y todo revienta en la vuelta siguiente.
5. Usar filter(...)[0] en lugar de find(...). Recorre la lista entera para quedarse con uno.
6. Comprobar el resultado de filter con if (resultado). Un array vacío es truthy: siempre entrará. Comprueba resultado.length === 0.
7. Comparar strings con < en lugar de localeCompare. Con acentos y mayúsculas obtienes órdenes incorrectos en español.
8. Usar every sobre una lista vacía como validación. Devuelve true siempre. Comprueba antes que la lista no esté vacía si eso importa.
9. Encadenar diez métodos sobre listas enormes. Cada eslabón es un recorrido y un array nuevo. Legibilidad primero; optimiza cuando midas (Módulo 9).
Consejo profesional. Estos métodos son un vocabulario, y usarlo bien es sobre todo decir lo que quieres. Si el nombre del método coincide con lo que dirías en voz alta —«búscame la primera», «quédate con las altas», «suma las horas»—, el código está bien. Si tienes que explicar un reduce de doce líneas, probablemente ahí había un bucle.
Ejercicios
Ejercicio 1 — Consultas del lunes. Con el backlog canónico, resuelve cada punto en una sola expresión:
- La tarea con
id5. - ¿Hay alguna tarea de Lucía sin revisor?
- ¿Todas las tareas abiertas tienen al menos una etiqueta?
- Los títulos de las tareas de prioridad media.
- La última tarea (recorriendo desde el final) que esté en curso.
Ejercicio 2 — El ranking de carga. Escribe rankingDeCarga(tareas) que devuelva un array de objetos { responsable, tareas, horas, vencidas } con una entrada por persona, contando solo tareas abiertas, ordenado por horas descendentes y, en caso de empate, por nombre. Usa reduce para agrupar y toSorted con criterio doble. Comprueba que Iván sale primero con 25 h y 1 vencida.
Ejercicio 3 — Reparto equilibrado. Marta quiere saber si el trabajo está repartido. Escribe estaEquilibrado(tareas, margen) que devuelva true si la diferencia entre las horas abiertas de la persona más cargada y la menos cargada no supera margen. Devuelve además, en un objeto, { equilibrado, maximo, minimo, diferencia }. Pruébalo con margen = 10 y con margen = 20.
Soluciones
Ejercicio 1
// 1. find: buscas UNA tarea concreta
console.log(backlog.find((t) => t.id === 5).titulo);
// 'Guía de encuadernación para residentes'
// 2. some: basta con que exista una
console.log(backlog.some((t) => t.responsable === 'Lucía' && t.revisor === null));
// false ← la tarea 3 de Lucía sí tiene revisor (Iván)
// 3. every sobre el subconjunto de abiertas
console.log(backlog.filter((t) => t.estado !== 'hecha').every((t) => t.etiquetas.length > 0));
// true
// 4. filter + map: seleccionar y transformar
console.log(backlog.filter((t) => t.prioridad === 'media').map((t) => t.titulo));
// [ 'Cartelería del taller de serigrafía', 'Guía de encuadernación para residentes' ]
// 5. findLast: recorre desde el final
console.log(backlog.findLast((t) => t.estado === 'en-curso').titulo);
// 'Guía de encuadernación para residentes'Errores frecuentes en este ejercicio: usar filter en el punto 1 (recorre de más y devuelve un array), y en el punto 3 aplicar every sobre el backlog completo en lugar de sobre las abiertas, lo que responde a otra pregunta.
Ejercicio 2
function rankingDeCarga(tareas, hoy = HOY) {
const porPersona = tareas
.filter((t) => t.estado !== 'hecha')
.reduce((acc, t) => {
if (!acc[t.responsable]) {
acc[t.responsable] = { responsable: t.responsable, tareas: 0, horas: 0, vencidas: 0 };
}
const fila = acc[t.responsable];
fila.tareas += 1;
fila.horas += t.horasEstimadas;
if (t.fechaLimite < hoy) fila.vencidas += 1;
return acc;
}, {});
return Object.values(porPersona).toSorted(
(a, b) => (b.horas - a.horas) || a.responsable.localeCompare(b.responsable, 'es')
);
}
console.log(rankingDeCarga(backlog));
// [ { responsable: 'Iván', tareas: 3, horas: 25, vencidas: 1 },
// { responsable: 'Lucía', tareas: 1, horas: 14, vencidas: 0 },
// { responsable: 'Marta', tareas: 1, horas: 6, vencidas: 0 } ]Tres cosas que merecen comentario. Primera: el acumulador es un objeto de objetos, y al final Object.values lo convierte en el array que pide el enunciado. Segunda: dentro del reduce se muta el objeto acumulador, y eso está bien porque ese objeto se ha creado dentro de la propia función y no lo comparte nadie; mutar es un problema cuando afecta a datos de fuera. Tercera: fíjate en que Marta aparece con 1 tarea y 6 h, no con 2: su «Inventario de tintas de serigrafía» está hecha y el filter inicial la descarta.
Ejercicio 3
function estaEquilibrado(tareas, margen) {
const horas = tareas
.filter((t) => t.estado !== 'hecha')
.reduce((acc, t) => {
acc[t.responsable] = (acc[t.responsable] ?? 0) + t.horasEstimadas;
return acc;
}, {});
const valores = Object.values(horas);
if (valores.length === 0) {
return { equilibrado: true, maximo: 0, minimo: 0, diferencia: 0 };
}
const maximo = valores.reduce((a, b) => (b > a ? b : a), valores[0]);
const minimo = valores.reduce((a, b) => (b < a ? b : a), valores[0]);
const diferencia = maximo - minimo;
return { equilibrado: diferencia <= margen, maximo, minimo, diferencia };
}
console.log(estaEquilibrado(backlog, 10));
// { equilibrado: false, maximo: 25, minimo: 6, diferencia: 19 }
console.log(estaEquilibrado(backlog, 20));
// { equilibrado: true, maximo: 25, minimo: 6, diferencia: 19 }Dos apuntes. El patrón máximo con reduce es el mismo de 02-02, y el valor inicial es valores[0] en lugar de 0 porque con un 0 fijo el mínimo saldría siempre 0. Y la comprobación de lista vacía es imprescindible: sin ella, valores[0] sería undefined y todas las comparaciones darían false. Ese tipo de caso límite es exactamente lo que probarás sistemáticamente en el Módulo 8 con Jest.
Con margen = 10 el reparto no está equilibrado: Iván acumula 25 h frente a las 6 h de Marta. Es justo el diagnóstico que Marta necesitaba para redistribuir el backlog.
Conclusión
Ya tienes el arsenal completo para consultar una lista. Para buscar: find y findIndex cuando quieres un elemento o su posición, findLast y findLastIndex cuando te interesa el más reciente, some y every cuando la respuesta es un sí o un no —con su cortocircuito y con la trampa del array vacío—, y filter cuando quieres una sublista, recordando que siempre devuelve un array y que un array vacío es truthy.
Para ordenar, sort con sus dos trampas grabadas a fuego: sin comparadora ordena como texto ([10, 9, 100] → [10, 100, 9]) y muta el original. La comparadora devuelve negativo, cero o positivo; a - b para números, localeCompare para texto en español, comparación directa de strings para las fechas ISO —el premio a aquella decisión de diseño del Módulo 1—, la resta invertida para el orden descendente y el encadenado con || para varios criterios. Y toSorted/toReversed para no destruir los datos al construir una vista.
Para agregar, reduce, entendido de verdad: es el patrón acumulador con nombre, su función reductora debe devolver siempre el acumulador y su valor inicial no es opcional. Lo has usado en los cuatro casos que cubren el 90 % de lo que necesitarás: sumar (48 h, 45 h abiertas, esfuerzo 124), contar por estado, agrupar por responsable (Iván 25 h, Lucía 14 h, Marta 6 h) y construir un índice por id. Junto a él, Object.groupBy para agrupar sin esfuerzo, Set para deduplicar las 12 etiquetas en 11 únicas y Map para diccionarios con claves dinámicas. Y sabes encadenar filter().toSorted().map() sabiendo lo que cuesta cada eslabón. El informe semanal de carga de Taller Nómada, que era el entregable pendiente desde el Módulo 1, ya está escrito.
Si algo se ha repetido de forma sospechosa en las últimas páginas, son líneas como par[0] y par[1] al recorrer Object.entries, o const [eliminada] = tareas.splice(...), o const { totales, carga } = informe. Todas ellas apuntan a lo mismo: hay una sintaxis para desempaquetar un array o un objeto en variables con nombre, y llevas tres lecciones usándola de refilón sin haberla estudiado. Empezamos por la mitad más sencilla en Desestructuración de Arrays, donde aprenderás a partir una fechaLimite en año, mes y día en una sola línea, a intercambiar variables sin auxiliar y a escribir por fin for (const [i, tarea] of backlog.entries()) entendiendo exactamente qué significa.
Curso de JavaScript: De Principiante a Avanzado
Módulo 1: Introducción a JavaScript
- ¿Qué es JavaScript?
- Configuración de tu Entorno de Desarrollo
- Tu Primer Programa en JavaScript
- Sintaxis y Conceptos Básicos de JavaScript
- Variables y Tipos de Datos
- Operadores Básicos
- Conversión de Tipos y Comparaciones
- El Proyecto del Curso: Nómada Tareas
Módulo 2: Estructuras de Control
- Sentencias Condicionales
- Bucles: for, while, do-while
- Sentencias Switch
- Control del Flujo: break, continue y Bucles Anidados
- Manejo de Errores con try-catch
Módulo 3: Funciones
- Definición y Llamada de Funciones
- Expresiones de Función y Funciones Flecha
- Parámetros y Valores de Retorno
- Ámbito y Closures
- Hoisting y el Contexto de Ejecución
- Funciones de Orden Superior
- Recursividad
Módulo 4: Objetos y Arrays
- Introducción a los Objetos
- Métodos de Objeto y la Palabra Clave
this - Arrays: Conceptos Básicos y Métodos
- Iteración sobre Arrays
- Buscar, Ordenar y Agregar Datos: find, sort y reduce
- Desestructuración de Arrays
- Desestructuración de Objetos, Spread y Rest
- JSON y Copias de Objetos
Módulo 5: Objetos y Funciones Avanzadas
- Prototipos y Herencia
- Clases y Programación Orientada a Objetos
- Encapsulación: Getters, Setters y Campos Privados
- Módulos e Importación/Exportación
- JavaScript Asíncrono: Callbacks
- Promesas y Async/Await
- El Bucle de Eventos y la Cola de Microtareas
- Iteradores y Generadores
Módulo 6: El Modelo de Objetos del Documento (DOM)
- Introducción al DOM
- Selección y Manipulación de Elementos del DOM
- Manejo de Eventos
- Propagación, Delegación y Eventos Personalizados
- Creación y Eliminación de Elementos del DOM
- Renderizado de Listas y Plantillas HTML
- Manejo y Validación de Formularios
Módulo 7: APIs del Navegador y Temas Avanzados
- Almacenamiento Local y de Sesión
- Fetch API y AJAX
- Peticiones Robustas: Errores, Timeouts y AbortController
- WebSockets
- Service Workers y Aplicaciones Web Progresivas (PWAs)
- APIs del Navegador Esenciales
- Introducción a WebAssembly
Módulo 8: Pruebas y Depuración
- Depuración de JavaScript
- Calidad de Código: ESLint, Prettier y Convenciones
- Pruebas Unitarias con Jest
- Dobles de Prueba: Mocks, Stubs y Spies
- Pruebas de Integración
- Pruebas de Extremo a Extremo con Cypress
Módulo 9: Rendimiento y Optimización
- Medir Antes de Optimizar: DevTools y Web Vitals
- Optimización del Rendimiento de JavaScript
- Gestión de Memoria
- Manipulación Eficiente del DOM
- Carga Perezosa y División de Código
Módulo 10: Frameworks y Librerías de JavaScript
- Por Qué Existen los Frameworks
- Introducción a React
- Gestión de Estado con Redux
- Conceptos Básicos de Vue.js
- Conceptos Básicos de Angular
- Elegir el Framework Adecuado
