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

  1. El punto de partida: el backlog canónico
  2. Buscar un elemento: find, findIndex, findLast
  3. Comprobar el conjunto: some y every
  4. Seleccionar varios: filter
  5. La tabla de la búsqueda
  6. sort: la trampa de los números
  7. La función comparadora
  8. Ordenar por número, por texto y por fecha
  9. Orden descendente y ordenación por varios criterios
  10. toSorted y toReversed: ordenar sin mutar
  11. reduce, despacio
  12. Cuatro casos reales de reduce
  13. Object.groupBy, Map y Set
  14. Encadenar filter().map().sort() y su coste
  15. Ejemplo integrador: el informe semanal de carga
  16. Errores Comunes y Consejos
  17. Ejercicios
  18. Conclusión

  1. 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.

  1. Buscar un elemento: find, findIndex, findLast

find(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);              // undefined

Compá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));       // -1

Es 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 alta

Recuerda 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  ✓

  1. Comprobar el conjunto: some y every

A 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 cumplen

Ese 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

  1. Seleccionar varios: filter

filter(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 undefined

Ese ú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);

  1. La tabla de la búsqueda

Método Qué devuelve ¿Para pronto? Úsalo cuando…
find(p) El primer elemento que cumple, o undefined Buscas una tarea concreta
findIndex(p) Su posición, o -1 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 Buscas un primitivo exacto
indexOf(v) Posición o -1 Buscas la posición de un primitivo

  1. sort: la trampa de los números

Empecemos 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, destrozado

Dos reglas que se derivan de esto y que debes aplicar siempre:

  1. A sort se le pasa siempre una función comparadora. Sin excepciones, ni siquiera con números.
  2. Copia antes de ordenar, salvo que quieras mutar de verdad: array.slice().sort(cmp) o, mejor, array.toSorted(cmp).

  1. 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.

  1. 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 14h

Por 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.

  1. 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);   // 14

Cuando 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.

  1. toSorted y toReversed: ordenar sin mutar

Los 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);

  1. reduce, despacio

reduce es el método más potente del arsenal y el que peor se explica. La idea, en una frase:

reduce recorre 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) => nuevoAcumulado

La 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);      // 48

La 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 suma

Y 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));    // ✓ 0

Regla sin excepciones: pon siempre el valor inicial. Y elígelo con cuidado: 0 para sumar, 1 para 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; }, []);

  1. Cuatro casos reales de reduce

Caso 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); // 45

Caso 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.

  1. Object.groupBy, Map y Set

Object.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

  1. Encadenar filter().map().sort() y su coste

Como 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.

  1. 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:

  1. La tarea con id 5.
  2. ¿Hay alguna tarea de Lucía sin revisor?
  3. ¿Todas las tareas abiertas tienen al menos una etiqueta?
  4. Los títulos de las tareas de prioridad media.
  5. 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

Módulo 2: Estructuras de Control

Módulo 3: Funciones

Módulo 4: Objetos y Arrays

Módulo 5: Objetos y Funciones Avanzadas

Módulo 6: El Modelo de Objetos del Documento (DOM)

Módulo 7: APIs del Navegador y Temas Avanzados

Módulo 8: Pruebas y Depuración

Módulo 9: Rendimiento y Optimización

Módulo 10: Frameworks y Librerías de JavaScript

Módulo 11: Proyecto Final

© Copyright 2026. Todos los derechos reservados