En tancar la lliçó anterior va quedar una llista de feines pendents molt concreta: cercar la primera tasca que compleix una condició et va costar una funció sencera, agrupar per estat et va costar un bucle dins d'un altre, el resum et va costar tres variables acumuladores i ordenar el tauler per data límit ni tan sols sabies fer-ho. Totes aquestes operacions són tan universals que JavaScript les porta resoltes i amb nom propi. En aquesta lliçó aprendràs l'arsenal complet per consultar una llista: find, findIndex, findLast, some, every i filter per cercar i seleccionar; sort per ordenar —amb el parany més famós del llenguatge, que fa que [10, 9, 100] s'ordeni malament—; i reduce, l'eina més potent i la pitjor explicada de totes, que veurem a poc a poc i en quatre casos reals. Al final muntaràs l'informe setmanal de càrrega de Taller Nómada, que és l'entregable que la Marta demana des del Mòdul 1.

Contingut

  1. El punt de partida: el backlog canònic
  2. Cercar un element: find, findIndex, findLast
  3. Comprovar el conjunt: some i every
  4. Seleccionar-ne uns quants: filter
  5. La taula de la cerca
  6. sort: el parany dels números
  7. La funció comparadora
  8. Ordenar per número, per text i per data
  9. Ordre descendent i ordenació per diversos criteris
  10. toSorted i toReversed: ordenar sense mutar
  11. reduce, a poc a poc
  12. Quatre casos reals de reduce
  13. Object.groupBy, Map i Set
  14. Encadenar filter().map().sort() i el seu cost
  15. Exemple integrador: l'informe setmanal de càrrega
  16. Errors Habituals i Consells
  17. Exercicis
  18. Conclusió

  1. El punt de partida: el backlog canònic

Tot el que ve a continuació treballa sobre el mateix backlog de 04-01. El repetim en versió compacta per tenir-lo a mà:

'use strict';

const AVUI = '2026-09-20';
const PESOS = { alta: 3, mitjana: 2, baixa: 1 };

const backlog = [
  { id: 1, titol: 'Redissenyar la sala polivalent',           responsable: 'Iván',  prioritat: 'alta',    estat: 'en-curs', etiquetes: ['espai', 'disseny'],                horesEstimades: 12, dataLimit: '2026-09-30', revisor: 'Marta' },
  { id: 2, titol: 'Cartelleria del taller de serigrafia',     responsable: 'Marta', prioritat: 'mitjana', estat: 'pendent', etiquetes: ['serigrafia', 'comunicació'],       horesEstimades: 6,  dataLimit: '2026-10-15', revisor: null },
  { id: 3, titol: 'Actualitzar el web de reserves',           responsable: 'Lucía', prioritat: 'alta',    estat: 'pendent', etiquetes: ['web', 'reserves'],                 horesEstimades: 14, dataLimit: '2026-10-02', revisor: 'Iván' },
  { id: 4, titol: 'Inventari de tintes de serigrafia',        responsable: 'Marta', prioritat: 'baixa',   estat: 'feta',    etiquetes: ['serigrafia', 'magatzem'],          horesEstimades: 3,  dataLimit: '2026-09-12', revisor: null },
  { id: 5, titol: 'Guia sobre enquadernació per a residents', responsable: 'Iván',  prioritat: 'mitjana', estat: 'en-curs', etiquetes: ['enquadernació', 'documentació'],   horesEstimades: 8,  dataLimit: '2026-11-05', revisor: 'Lucía' },
  { id: 6, titol: 'Pressupost de la fusteria',                responsable: 'Iván',  prioritat: 'alta',    estat: 'pendent', etiquetes: ['fusteria', 'compres'],             horesEstimades: 5,  dataLimit: '2026-09-05', revisor: 'Marta' }
];

Tots els mètodes d'aquesta lliçó reben una funció com a argument: són funcions d'ordre superior, exactament les que vas estudiar a 03-06. La funció que retorna true o false per decidir si un element «val» s'anomena predicat.

  1. Cercar un element: find, findIndex, findLast

find(predicat) retorna el primer element que compleix la condició, o undefined si no n'hi ha cap. I s'atura tan bon punt el troba.

const sobrecarregada = backlog.find((t) => t.horesEstimades > 10);
console.log(sobrecarregada.titol);     // 'Redissenyar la sala polivalent'

const inexistent = backlog.find((t) => t.horesEstimades > 100);
console.log(inexistent);               // undefined

Compara-ho amb la funció que vas escriure a mà a l'exercici 2 de la lliçó anterior:

// Abans: set línies
function primeraTascaSobrecarregada(tasques, limit) {
  for (const tasca of tasques) {
    if (tasca.horesEstimades > limit) return tasca;
  }
  return null;
}

// Ara: una
const primera = backlog.find((t) => t.horesEstimades > 10);

L'única diferència de comportament és que find retorna undefined i la teva funció retornava null. Com que el resultat pot no existir, protegeix-te sempre amb ?. o ??:

console.log(backlog.find((t) => t.responsable === 'Ningú')?.titol ?? 'sense resultats');
// 'sense resultats'

findIndex(predicat) retorna la posició en lloc de l'element, o -1:

console.log(backlog.findIndex((t) => t.id === 6));        // 5
console.log(backlog.findIndex((t) => t.id === 99));       // -1

És el que necessites quan modificaràs l'array per posició, per exemple amb splice:

function eliminarPerId(tasques, id) {
  const posicio = tasques.findIndex((t) => t.id === id);
  if (posicio === -1) return null;
  const [eliminada] = tasques.splice(posicio, 1);
  return eliminada;
}

findLast i findLastIndex fan el mateix recorrent des del final:

console.log(backlog.find((t) => t.prioritat === 'alta').titol);
// 'Redissenyar la sala polivalent'      ← la primera alta

console.log(backlog.findLast((t) => t.prioritat === 'alta').titol);
// 'Pressupost de la fusteria'           ← l'última alta

Recorda a més la limitació d'includes i indexOf que vas veure a 04-03: comparen amb === i per això no serveixen amb objectes. find sí, perquè la comparació l'escrius tu:

console.log(backlog.includes({ id: 6 }));                    // false ✗
console.log(backlog.find((t) => t.id === 6) !== undefined);  // true  ✓

  1. Comprovar el conjunt: some i every

De vegades no vols l'element, sinó una resposta de sí o no sobre tota la llista.

// some: n'hi ha COM A MÍNIM UN que ho compleixi?
const hiHaVencudes = backlog.some((t) => t.dataLimit < AVUI && t.estat !== 'feta');
console.log(hiHaVencudes);      // true  (la 6, «Pressupost de la fusteria»)

// every: ho compleixen TOTS?
const totesTenenResponsable = backlog.every((t) => typeof t.responsable === 'string');
console.log(totesTenenResponsable);    // true

const totesRevisades = backlog.every((t) => t.revisor !== null);
console.log(totesRevisades);           // false  (les tasques 2 i 4 no tenen revisor)

Totes dues fan curtcircuit, igual que els operadors && i || de 01-06: some s'atura al primer true, every s'atura al primer false. Això les converteix en la forma correcta —i la més ràpida— de validar.

Dos comportaments amb l'array buit que sorprenen i convé conèixer:

console.log([].some((t) => true));    // false  ← no n'hi ha cap que ho compleixi
console.log([].every((t) => false));  // true   ← "tots" els zero elements ho compleixen

Aquest true s'anomena veritat vàcua i és matemàticament correcte («tots els unicorns d'aquesta llista són blaus» és veritat si la llista és buida). En codi, tingues-ho present: every sobre una llista buida mai no avisa de res.

Aplicació al projecte, validant el backlog abans de desar-lo:

function backlogValid(tasques) {
  return tasques.every((t) =>
    typeof t.id === 'number' &&
    typeof t.titol === 'string' && t.titol.trim() !== '' &&
    t.horesEstimades > 0 && t.horesEstimades <= 40 &&          // R3
    Array.isArray(t.etiquetes)
  );
}

console.log(backlogValid(backlog));                            // true
console.log(backlogValid([{ id: 9, titol: '', horesEstimades: 0, etiquetes: [] }]));   // false

  1. Seleccionar-ne uns quants: filter

filter(predicat) retorna un array nou amb tots els elements que compleixen la condició. No muta l'original i, a diferència de map, la longitud del resultat sí que canvia.

const obertes = backlog.filter((t) => t.estat !== 'feta');
console.log(obertes.length);        // 5

const altesObertes = backlog.filter((t) => t.prioritat === 'alta' && t.estat !== 'feta');
console.log(altesObertes.map((t) => t.id));   // [ 1, 3, 6 ]

const deIvan = backlog.filter((t) => t.responsable === 'Iván');
console.log(deIvan.length);         // 3

const buit = backlog.filter((t) => t.horesEstimades > 100);
console.log(buit);                  // []   ← array buit, no undefined

Aquest últim detall és important: filter mai no retorna undefined, sempre un array (possiblement buit). Per això filter(...).length === 0 és la comprovació correcta, i no if (resultat), que seria sempre cert perquè un array buit és truthy (01-07).

Diferències amb find, que sovint es confonen:

find filter
Retorna Un element o undefined Un array (possiblement buit)
Recorre sencer? No: s'atura al primer Sí, sempre
Quan fer-lo servir Cerques un de concret (per id) Vols tots els que ho compleixin

Un error freqüent és fer servir filter quan volies find:

// ✗ Recorre les sis tasques i retorna un array d'un element
const tasca = backlog.filter((t) => t.id === 3)[0];

// ✓ S'atura tan bon punt la troba i retorna la tasca
const tasca2 = backlog.find((t) => t.id === 3);

  1. La taula de la cerca

Mètode Què retorna S'atura aviat? Fes-lo servir quan…
find(p) El primer element que ho compleix, o undefined Cerques una tasca concreta
findIndex(p) La seva posició, o -1 Modificaràs l'array per índex
findLast(p) L'últim que ho compleix, o undefined Sí (des del final) T'interessa el més recent
findLastIndex(p) La seva posició, o -1 Sí (des del final) Igual, però necessites la posició
some(p) true / false Sí, al primer true «Hi ha alguna vençuda?»
every(p) true / false Sí, al primer false «Són totes vàlides?»
filter(p) Array amb tots els que ho compleixen No Vols una subllista
includes(v) true / false Cerques un primitiu exacte
indexOf(v) Posició o -1 Cerques la posició d'un primitiu

  1. sort: el parany dels números

Comencem pel desastre, perquè és la millor manera que no se t'oblidi:

const numeros = [10, 9, 100, 1, 25];
numeros.sort();
console.log(numeros);        // [ 1, 10, 100, 25, 9 ]   ✗ què??

No és un error del motor. sort() sense arguments converteix cada element a string i els compara alfabèticament, caràcter a caràcter. Com que '1' va abans que '2' i '2' abans que '9', el resultat és un ordre de diccionari: '1', '10', '100', '25', '9'.

I hi ha un segon parany igual d'important: sort muta l'array original, com ja anunciava la taula de 04-03.

const hores = [12, 6, 14, 3, 8, 5];
const ordenades = hores.sort();
console.log(hores === ordenades);   // true  ← és el MATEIX array, no una còpia
console.log(hores);                 // [ 12, 14, 3, 5, 6, 8 ]  ← l'original, destrossat

Dues regles que se'n deriven i que has d'aplicar sempre:

  1. A sort se li passa sempre una funció comparadora. Sense excepcions, ni tan sols amb números.
  2. Copia abans d'ordenar, tret que vulguis mutar de debò: array.slice().sort(cmp) o, millor, array.toSorted(cmp).

  1. La funció comparadora

La comparadora rep dos elements i retorna un número que respon a la pregunta «quin va abans?»:

Retorna Significat
Un número negatiu a va abans que b
Zero Tant és l'ordre entre ells
Un número positiu a va després que b
const hores = [12, 6, 14, 3, 8, 5];

// Ascendent: si a - b és negatiu, a és menor i va abans
console.log(hores.toSorted((a, b) => a - b));    // [ 3, 5, 6, 8, 12, 14 ]

// Descendent: s'inverteix la resta
console.log(hores.toSorted((a, b) => b - a));    // [ 14, 12, 8, 6, 5, 3 ]

El truc a - b funciona perquè la resta ja produeix el signe correcte: si a és menor, la resta és negativa. Només serveix amb números; amb strings cal comparar d'una altra manera (apartat 8).

Una comparadora escrita a la llarga, per veure què passa per dins:

function perHoresAscendent(a, b) {
  if (a.horesEstimades < b.horesEstimades) return -1;
  if (a.horesEstimades > b.horesEstimades) return 1;
  return 0;
}

I un avís: la comparadora ha de ser coherent. Si dius que A va abans que B i també que B va abans que A, el resultat és imprevisible. No facis servir mai Math.random() dins d'una comparadora per «barrejar»: pot produir ordres esbiaixats o fins i tot errors.

  1. Ordenar per número, per text i per data

Per número (el cas de horesEstimades i id):

const perHores = backlog.toSorted((a, b) => a.horesEstimades - b.horesEstimades);
console.log(perHores.map((t) => `${t.horesEstimades}h`).join(' '));
// 3h 5h 6h 8h 12h 14h

Per text. La resta no serveix amb strings ('a' - 'b' és NaN). I l'operador < compara per codi de caràcter, cosa que col·loca malament les majúscules i els accents. La resposta correcta és localeCompare, que compara segons les regles de l'idioma:

console.log('à'.localeCompare('b', 'ca'));      // -1  ← 'à' va abans que 'b' en català
console.log('à' < 'b');                          // false ✗ comparació per codi

const perTitol = backlog.toSorted((a, b) => a.titol.localeCompare(b.titol, 'ca'));
console.log(perTitol.map((t) => t.id));          // [ 3, 2, 5, 4, 6, 1 ]
console.log(perTitol[0].titol);                  // 'Actualitzar el web de reserves'
console.log(perTitol[2].titol);                  // 'Guia sobre enquadernació per a residents'

localeCompare retorna un número negatiu, zero o positiu: exactament el que necessita sort. El segon argument és l'idioma; el tercer admet opcions com { sensitivity: 'base' } per ignorar majúscules i accents:

const noms = ['Iván', 'ana', 'Álvaro', 'Bruno'];
console.log(noms.toSorted((a, b) => a.localeCompare(b, 'ca', { sensitivity: 'base' })));
// [ 'Álvaro', 'ana', 'Bruno', 'Iván' ]

Per data. Aquí es recull el fruit d'una decisió de disseny del Mòdul 1: dataLimit és un string ISO 'yyyy-mm-dd', i aquest format s'ordena alfabèticament igual que cronològicament. Per això n'hi ha prou de comparar strings:

const perData = backlog.toSorted((a, b) => a.dataLimit.localeCompare(b.dataLimit));
console.log(perData.map((t) => `${t.dataLimit} (${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)

Amb el format '30/09/2026' això seria impossible sense convertir abans cada data. Aquest és exactament el motiu pel qual el model va triar ISO.

  1. Ordre descendent i ordenació per diversos criteris

Per invertir l'ordre, inverteix la comparadora (no ordenis i després apliquis reverse, que són dues passades per a res):

const mesUrgentsPrimer = backlog.toSorted((a, b) => b.horesEstimades - a.horesEstimades);
console.log(mesUrgentsPrimer[0].horesEstimades);   // 14

Quan hi ha empats, es passa al criteri següent. La tècnica és avaluar el primer criteri i, si dóna 0, retornar el segon:

const ORDRE_PRIORITAT = { alta: 0, mitjana: 1, baixa: 2 };

function perPrioritatIData(a, b) {
  const perPrioritat = ORDRE_PRIORITAT[a.prioritat] - ORDRE_PRIORITAT[b.prioritat];
  if (perPrioritat !== 0) return perPrioritat;
  return a.dataLimit.localeCompare(b.dataLimit);    // desempat per data
}

const tauler = backlog.toSorted(perPrioritatIData);
console.log(tauler.map((t) => `${t.prioritat} ${t.dataLimit} [${t.id}]`).join('\n'));
// alta 2026-09-05 [6]
// alta 2026-09-30 [1]
// alta 2026-10-02 [3]
// mitjana 2026-10-15 [2]
// mitjana 2026-11-05 [5]
// baixa 2026-09-12 [4]

Fixa't en l'objecte ORDRE_PRIORITAT: és el mateix patró de diccionari de consulta de 04-01, aquí fet servir per convertir un text ('alta') en un número ordenable. Sense ell, 'alta' < 'baixa' < 'mitjana' donaria l'ordre alfabètic, que no és el que vol la Marta.

Amb més de dos criteris, el patró es generalitza encadenant amb ||, aprofitant que 0 és falsy:

const perTresCriteris = (a, b) =>
  (ORDRE_PRIORITAT[a.prioritat] - ORDRE_PRIORITAT[b.prioritat]) ||
  a.dataLimit.localeCompare(b.dataLimit) ||
  a.titol.localeCompare(b.titol, 'ca');

Es llegeix així: «fes servir el primer criteri; si dóna 0, prova el segon; si també dóna 0, el tercer». És una aplicació directa del curtcircuit de || que vas estudiar a 01-06.

Un últim detall: des d'ES2019, sort és estable, cosa que significa que els elements que empaten conserven el seu ordre original. Per això ordenar primer per data i després per prioritat també produeix un resultat correcte, encara que el criteri combinat és més clar.

  1. toSorted i toReversed: ordenar sense mutar

Els has fet servir a tot l'apartat anterior. Són les versions immutables de sort i reverse, disponibles en JavaScript modern:

const original = [12, 6, 14];

const ordenat = original.toSorted((a, b) => a - b);
console.log(ordenat);       // [ 6, 12, 14 ]
console.log(original);      // [ 12, 6, 14 ]  ✓ intacte

const alReves = original.toReversed();
console.log(alReves);       // [ 14, 6, 12 ]
console.log(original);      // [ 12, 6, 14 ]  ✓ intacte
Mutant Immutable Què fa
sort(cmp) toSorted(cmp) Ordena
reverse() toReversed() Inverteix
splice(i, n, v) toSpliced(i, n, v) Esborra/insereix
arr[i] = v with(i, v) Reemplaça una posició

Si treballes en un entorn antic que no els tingui, l'equivalent és copiar abans: array.slice().sort(cmp). El que mai no has de fer és ordenar directament el backlog per mostrar-lo d'una manera concreta: la llista de dades i la llista de presentació són coses diferents.

// ✗ Ordena el backlog de debò; la resta de l'aplicació veu un altre ordre
backlog.sort((a, b) => a.horesEstimades - b.horesEstimades);

// ✓ Produeix una vista ordenada, sense tocar les dades
const vista = backlog.toSorted((a, b) => a.horesEstimades - b.horesEstimades);

  1. reduce, a poc a poc

reduce és el mètode més potent de l'arsenal i el que pitjor s'explica. La idea, en una frase:

reduce recorre l'array portant un valor acumulat, i retorna aquest valor al final.

Ni més ni menys. És exactament el patró acumulador que vas aprendre a 02-02, empaquetat en un mètode. La seva signatura té dues parts:

array.reduce(funcioReductora, valorInicial)
//           ↑ (acumulat, element, index, array) => nouAcumulat

La versió més senzilla, sumar hores, comparada amb el bucle equivalent:

// Amb bucle (el que ja sabies fer)
let total = 0;
for (const tasca of backlog) {
  total += tasca.horesEstimades;
}
console.log(total);       // 48

// Amb reduce (el mateix, comprimit)
const total2 = backlog.reduce((acc, tasca) => acc + tasca.horesEstimades, 0);
console.log(total2);      // 48

La correspondència peça per peça:

Al bucle A reduce
let total = 0 El valor inicial, el segon argument
total += ... El que retorna la funció reductora
La variable total al final El valor de retorn de reduce

Seguim l'execució volta a volta, que és l'única manera que s'entengui de debò:

Volta acc que entra tasca.horesEstimades Retorna
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"]

Per què el valor inicial importa tant. Si l'omets, reduce fa servir el primer element com a acumulador inicial i comença a recórrer des del segon. Amb números solts això encara funciona; amb objectes, no:

console.log([12, 6, 14].reduce((a, b) => a + b));        // 32 ✓ funciona per casualitat

// ✗ Sense inicial, acc és l'objecte tasca complet a la primera volta
console.log(backlog.reduce((acc, t) => acc + t.horesEstimades));
// '[object Object]6143850'   ← concatenació de strings, no una suma

I hi ha un cas encara pitjor: sense valor inicial, un array buit llança 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 sense excepcions: posa sempre el valor inicial. I tria'l amb cura: 0 per sumar, 1 per multiplicar, '' per concatenar, [] per construir una llista, {} per construir un objecte.

L'altre error clàssic, ja avisat a 03-06, és oblidar retornar l'acumulador:

// ✗ La funció no retorna res: acc passa a ser undefined a la volta 2
const malament = backlog.reduce((acc, t) => { acc.push(t.titol); }, []);
// ✗ TypeError: Cannot read properties of undefined (reading 'push')

// ✓ Retornar sempre l'acumulador
const correcte = backlog.reduce((acc, t) => { acc.push(t.titol); return acc; }, []);

  1. Quatre casos reals de reduce

Cas 1: sumar amb condició i pes. L'esforç ponderat del backlog, el número 124 que arrossegues des del Mòdul 3:

const esforc = backlog.reduce((acc, t) => acc + PESOS[t.prioritat] * t.horesEstimades, 0);
console.log(esforc);        // 124

// I les hores obertes, filtrant dins del mateix reduce
const horesObertes = backlog.reduce(
  (acc, t) => (t.estat === 'feta' ? acc : acc + t.horesEstimades),
  0
);
console.log(horesObertes);  // 45

Cas 2: comptar per estat. L'acumulador és un objecte:

const perEstat = backlog.reduce((acc, t) => {
  acc[t.estat] = (acc[t.estat] ?? 0) + 1;
  return acc;
}, {});

console.log(perEstat);      // { 'en-curs': 2, pendent: 3, feta: 1 }

Dos detalls del codi: acc[t.estat] fa servir claudàtors perquè la clau és dins d'una variable (04-01), i (acc[...] ?? 0) resol la primera aparició, quan la clau encara no existeix i retornaria undefined.

Cas 3: agrupar tasques per responsable. L'acumulador és un objecte els valors del qual són arrays:

const perResponsable = backlog.reduce((acc, t) => {
  if (!acc[t.responsable]) acc[t.responsable] = [];
  acc[t.responsable].push(t);
  return acc;
}, {});

console.log(Object.keys(perResponsable));               // [ 'Iván', 'Marta', 'Lucía' ]
console.log(perResponsable['Iván'].length);             // 3
console.log(perResponsable['Iván'].map((t) => t.id));   // [ 1, 5, 6 ]
console.log(perResponsable['Lucía'][0].titol);          // 'Actualitzar el web de reserves'

I la variant que interessa a la Marta, agrupar sumant hores en lloc d'acumular tasques:

const horesPerPersona = backlog.reduce((acc, t) => {
  if (t.estat === 'feta') return acc;
  acc[t.responsable] = (acc[t.responsable] ?? 0) + t.horesEstimades;
  return acc;
}, {});

console.log(horesPerPersona);    // { Iván: 25, Marta: 6, Lucía: 14 }

25 + 6 + 14 = 45, les hores obertes del backlog. És exactament el que vas fer a mà amb bucles a l'exercici 3 de 04-02, ara en cinc línies.

Cas 4: construir un índex per id. El diccionari que a 04-01 vas muntar amb un for:

const index = backlog.reduce((acc, t) => {
  acc[t.id] = t;
  return acc;
}, {});

console.log(index[3].titol);    // 'Actualitzar el web de reserves'
console.log(index[99]?.titol);  // undefined

Per què molestar-s'hi? Perquè backlog.find((t) => t.id === 3) recorre la llista cada vegada que cerques, mentre que index[3] és un accés directe. Amb sis tasques tant és; al Mòdul 9 veuràs quan comença a importar.

Un advertiment d'estil, que també vam fer a 03-06 amb les funcions d'ordre superior: reduce no és sempre la millor opció. Si el que escrius amb reduce és més difícil de llegir que el bucle equivalent, escriu el bucle. reduce brilla en sumes, recomptes, agrupacions i índexs; es torna il·legible quan algú el fa servir per fer el que fan map o filter.

  1. Object.groupBy, Map i Set

Object.groupBy (JavaScript modern) fa exactament el cas 3 sense reduce:

const perPrioritat = Object.groupBy(backlog, (t) => t.prioritat);

console.log(Object.keys(perPrioritat));                   // [ 'alta', 'mitjana', 'baixa' ]
console.log(perPrioritat.alta.map((t) => t.id));          // [ 1, 3, 6 ]
console.log(perPrioritat.mitjana.length);                 // 2
console.log(perPrioritat.baixa[0].titol);                 // 'Inventari de tintes de serigrafia'

És més llegible que el reduce equivalent, però és recent: comprova el suport del teu entorn abans de fer-lo servir en producció (al Mòdul 8 veuràs com es comproven aquestes coses). També existeix Map.groupBy, que retorna un Map en lloc d'un objecte.

Set és una col·lecció sense duplicats. És la resposta a la pregunta que vam deixar oberta a 04-03: com desduplicar les etiquetes del backlog.

const totes = backlog.flatMap((t) => t.etiquetes);
console.log(totes.length);                    // 12

const uniques = new Set(totes);
console.log(uniques.size);                    // 11  ← 'serigrafia' hi era dues vegades
console.log(uniques.has('serigrafia'));       // true
console.log(uniques.has('màrqueting'));       // false

// Convertir-lo de tornada a array (amb Array.from, de 04-03)
const llistaUnica = Array.from(uniques).toSorted((a, b) => a.localeCompare(b, 'ca'));
console.log(llistaUnica);
// [ 'compres', 'comunicació', 'disseny', 'documentació', 'enquadernació', 'espai',
//   'fusteria', 'magatzem', 'reserves', 'serigrafia', 'web' ]

L'idioma Array.from(new Set(array)) és la manera estàndard d'eliminar duplicats en JavaScript, i val la pena memoritzar-lo.

Map és un diccionari les claus del qual poden ser de qualsevol tipus, conserva l'ordre d'inserció i sap quantes entrades té:

const recompte = new Map();
for (const t of backlog) {
  recompte.set(t.responsable, (recompte.get(t.responsable) ?? 0) + 1);
}

console.log(recompte.get('Iván'));    // 3
console.log(recompte.size);           // 3
console.log(Array.from(recompte.entries()));
// [ [ 'Iván', 3 ], [ 'Marta', 2 ], [ 'Lucía', 1 ] ]

Quan triar cada estructura:

Necessites Fes servir
Agrupar camps amb nom d'una entitat Objecte literal
Diccionari amb claus de text conegudes Objecte literal
Diccionari amb claus dinàmiques, o portar el compte Map
Claus que no són strings (números de debò, objectes) Map
Una col·lecció sense duplicats Set

  1. Encadenar filter().map().sort() i el seu cost

Com que filter, map, toSorted i flatMap retornen arrays, es poden encadenar, i el resultat es llegeix gairebé com una frase:

const informeAltes = backlog
  .filter((t) => t.prioritat === 'alta' && t.estat !== 'feta')
  .toSorted((a, b) => a.dataLimit.localeCompare(b.dataLimit))
  .map((t) => `${t.dataLimit} · ${t.titol} (${t.responsable}, ${t.horesEstimades} h)`);

console.log(informeAltes.join('\n'));
// 2026-09-05 · Pressupost de la fusteria (Iván, 5 h)
// 2026-09-30 · Redissenyar la sala polivalent (Iván, 12 h)
// 2026-10-02 · Actualitzar el web de reserves (Lucía, 14 h)

Llegeix-ho de dalt a baix: queda't amb les de prioritat alta obertes, ordena-les per data, converteix-les en text. Cada pas té un nom i es pot llegir per separat, que és just el que buscaves a 03-06 amb filtrar → transformar → agregar.

El cost: cada baula de la cadena recorre l'array sencer i en crea un de nou. Tres mètodes encadenats són tres recorreguts i tres arrays intermedis, mentre que un for faria una sola passada sense crear res.

Cadena de tres mètodes Bucle únic
Recorreguts 3 1
Arrays intermedis 2 0
Llegibilitat Molt alta Mitjana

Amb sis tasques, la diferència és literalment inapreciable. Amb centenars de milers d'elements comença a notar-se, i aquí entren les tècniques d'Optimització del Rendiment. El consell, avui: encadena i prioritza la llegibilitat, i optimitza només quan mesuris que cal. Un detall gratuït: posa el filter primer a la cadena, perquè els passos següents treballin sobre menys elements.

  1. Exemple integrador: l'informe setmanal de càrrega

La Marta obre el panell cada dilluns i vol una foto completa del backlog. Amb el que has après, cap en una funció.

'use strict';

function informeSetmanal(tasques, avui) {
  const obertes = tasques.filter((t) => t.estat !== 'feta');

  const totals = {
    tasques: tasques.length,
    obertes: obertes.length,
    horesTotals: tasques.reduce((acc, t) => acc + t.horesEstimades, 0),
    horesObertes: obertes.reduce((acc, t) => acc + t.horesEstimades, 0),
    esforc: tasques.reduce((acc, t) => acc + PESOS[t.prioritat] * t.horesEstimades, 0)
  };

  const carrega = obertes.reduce((acc, t) => {
    acc[t.responsable] = (acc[t.responsable] ?? 0) + t.horesEstimades;
    return acc;
  }, {});

  const vencudes = obertes
    .filter((t) => t.dataLimit < avui)
    .toSorted((a, b) => a.dataLimit.localeCompare(b.dataLimit));

  const properes = obertes
    .toSorted((a, b) => a.dataLimit.localeCompare(b.dataLimit))
    .slice(0, 3);

  const etiquetes = Array.from(new Set(tasques.flatMap((t) => t.etiquetes)))
    .toSorted((a, b) => a.localeCompare(b, 'ca'));

  return { totals, carrega, vencudes, properes, etiquetes, hiHaRisc: vencudes.length > 0 };
}

function imprimirInforme(informe, avui) {
  const { totals, carrega, vencudes, properes, etiquetes } = informe;

  console.log(`═══ Informe setmanal · Taller Nómada · ${avui} ═══`);
  console.log(`${totals.obertes} de ${totals.tasques} tasques obertes · ` +
              `${totals.horesObertes} h de ${totals.horesTotals} h · ` +
              `esforç ponderat ${totals.esforc}`);

  console.log('\nCàrrega per persona:');
  Object.entries(carrega)
    .toSorted((a, b) => b[1] - a[1])
    .forEach((parell) => console.log(`  ${parell[0].padEnd(6)} ${parell[1]} h`));

  console.log(`\nVençudes (${vencudes.length}):`);
  if (vencudes.length === 0) console.log('  — cap —');
  vencudes.forEach((t) => console.log(`  ⚠ ${t.dataLimit} ${t.titol} (${t.responsable})`));

  console.log('\nPropers venciments:');
  properes.forEach((t) => console.log(`  ${t.dataLimit} ${t.titol}`));

  console.log(`\nEtiquetes en ús (${etiquetes.length}): ${etiquetes.join(', ')}`);
}

imprimirInforme(informeSetmanal(backlog, AVUI), AVUI);

Sortida:

═══ Informe setmanal · Taller Nómada · 2026-09-20 ═══
5 de 6 tasques obertes · 45 h de 48 h · esforç ponderat 124

Càrrega per persona:
  Iván   25 h
  Lucía  14 h
  Marta  6 h

Vençudes (1):
  ⚠ 2026-09-05 Pressupost de la fusteria (Iván)

Propers venciments:
  2026-09-05 Pressupost de la fusteria
  2026-09-30 Redissenyar la sala polivalent
  2026-10-02 Actualitzar el web de reserves

Etiquetes en ús (11): compres, comunicació, disseny, documentació, enquadernació, espai, fusteria, magatzem, reserves, serigrafia, web

Repassa quin mètode resol cada bloc: filter per a les obertes i les vençudes, reduce per als tres totals i per a la càrrega per persona, toSorted amb localeCompare per a l'ordre per data i per text, slice(0, 3) per quedar-se amb els tres propers venciments, flatMap + Set per a les etiquetes úniques i Object.entries + toSorted per ordenar l'objecte de càrrega per hores descendents. Compara-ho amb el motor d'informes que vas muntar a 03-06 sobre arrays paral·lels: la lògica és la mateixa, però cap en una pantalla i es llegeix.

(La línia const { totals, carrega, ... } = informe; és desestructuració d'objectes, i és el tema de 04-07. Llegeix-la de moment com «treu aquestes propietats a variables soltes».)

Errors Habituals i Consells

1. Cridar sort() sense comparadora. Ordena com a text: [10, 9, 100] es converteix en [10, 100, 9]. Passa-hi sempre (a, b) => a - b o la comparadora que toqui.

2. Oblidar que sort i reverse muten. Fes servir toSorted/toReversed, o copia amb slice() abans.

3. Ometre el valor inicial de reduce. Amb objectes produeix brossa i amb arrays buits llança TypeError.

4. Oblidar el return a la funció reductora. L'acumulador passa a undefined i tot peta a la volta següent.

5. Fer servir filter(...)[0] en lloc de find(...). Recorre la llista sencera per quedar-se amb un.

6. Comprovar el resultat de filter amb if (resultat). Un array buit és truthy: sempre hi entrarà. Comprova resultat.length === 0.

7. Comparar strings amb < en lloc de localeCompare. Amb accents i majúscules obtens ordres incorrectes en català.

8. Fer servir every sobre una llista buida com a validació. Retorna true sempre. Comprova abans que la llista no estigui buida si això importa.

9. Encadenar deu mètodes sobre llistes enormes. Cada baula és un recorregut i un array nou. Llegibilitat primer; optimitza quan mesuris (Mòdul 9).

Consell professional. Aquests mètodes són un vocabulari, i fer-lo servir bé és sobretot dir el que vols. Si el nom del mètode coincideix amb el que diries en veu alta —«cerca'm la primera», «queda't amb les altes», «suma les hores»—, el codi està bé. Si has d'explicar un reduce de dotze línies, probablement allà hi havia un bucle.

Exercicis

Exercici 1 — Consultes del dilluns. Amb el backlog canònic, resol cada punt en una sola expressió:

  1. La tasca amb id 5.
  2. Hi ha alguna tasca de la Lucía sense revisor?
  3. Totes les tasques obertes tenen almenys una etiqueta?
  4. Els títols de les tasques de prioritat mitjana.
  5. L'última tasca (recorrent des del final) que estigui en curs.

Exercici 2 — El rànquing de càrrega. Escriu rankingDeCarrega(tasques) que retorni un array d'objectes { responsable, tasques, hores, vencudes } amb una entrada per persona, comptant només tasques obertes, ordenat per hores descendents i, en cas d'empat, per nom. Fes servir reduce per agrupar i toSorted amb criteri doble. Comprova que l'Iván surt primer amb 25 h i 1 vençuda.

Exercici 3 — Repartiment equilibrat. La Marta vol saber si la feina està repartida. Escriu estaEquilibrat(tasques, marge) que retorni true si la diferència entre les hores obertes de la persona més carregada i la menys carregada no supera marge. Retorna a més, en un objecte, { equilibrat, maxim, minim, diferencia }. Prova-ho amb marge = 10 i amb marge = 20.

Solucions

Exercici 1

// 1. find: cerques UNA tasca concreta
console.log(backlog.find((t) => t.id === 5).titol);
// 'Guia sobre enquadernació per a residents'

// 2. some: n'hi ha prou que n'existeixi una
console.log(backlog.some((t) => t.responsable === 'Lucía' && t.revisor === null));
// false  ← la tasca 3 de la Lucía sí que té revisor (Iván)

// 3. every sobre el subconjunt d'obertes
console.log(backlog.filter((t) => t.estat !== 'feta').every((t) => t.etiquetes.length > 0));
// true

// 4. filter + map: seleccionar i transformar
console.log(backlog.filter((t) => t.prioritat === 'mitjana').map((t) => t.titol));
// [ 'Cartelleria del taller de serigrafia', 'Guia sobre enquadernació per a residents' ]

// 5. findLast: recorre des del final
console.log(backlog.findLast((t) => t.estat === 'en-curs').titol);
// 'Guia sobre enquadernació per a residents'

Errors freqüents en aquest exercici: fer servir filter al punt 1 (recorre de més i retorna un array), i al punt 3 aplicar every sobre el backlog complet en lloc de sobre les obertes, cosa que respon a una altra pregunta.

Exercici 2

function rankingDeCarrega(tasques, avui = AVUI) {
  const perPersona = tasques
    .filter((t) => t.estat !== 'feta')
    .reduce((acc, t) => {
      if (!acc[t.responsable]) {
        acc[t.responsable] = { responsable: t.responsable, tasques: 0, hores: 0, vencudes: 0 };
      }
      const fila = acc[t.responsable];
      fila.tasques += 1;
      fila.hores += t.horesEstimades;
      if (t.dataLimit < avui) fila.vencudes += 1;
      return acc;
    }, {});

  return Object.values(perPersona).toSorted(
    (a, b) => (b.hores - a.hores) || a.responsable.localeCompare(b.responsable, 'ca')
  );
}

console.log(rankingDeCarrega(backlog));
// [ { responsable: 'Iván',  tasques: 3, hores: 25, vencudes: 1 },
//   { responsable: 'Lucía', tasques: 1, hores: 14, vencudes: 0 },
//   { responsable: 'Marta', tasques: 1, hores: 6,  vencudes: 0 } ]

Tres coses que mereixen comentari. Primera: l'acumulador és un objecte d'objectes, i al final Object.values el converteix en l'array que demana l'enunciat. Segona: dins del reduce es muta l'objecte acumulador, i això està bé perquè aquest objecte s'ha creat dins de la mateixa funció i no el comparteix ningú; mutar és un problema quan afecta dades de fora. Tercera: fixa't que la Marta apareix amb 1 tasca i 6 h, no amb 2: el seu «Inventari de tintes de serigrafia» està fet i el filter inicial el descarta.

Exercici 3

function estaEquilibrat(tasques, marge) {
  const hores = tasques
    .filter((t) => t.estat !== 'feta')
    .reduce((acc, t) => {
      acc[t.responsable] = (acc[t.responsable] ?? 0) + t.horesEstimades;
      return acc;
    }, {});

  const valors = Object.values(hores);
  if (valors.length === 0) {
    return { equilibrat: true, maxim: 0, minim: 0, diferencia: 0 };
  }

  const maxim = valors.reduce((a, b) => (b > a ? b : a), valors[0]);
  const minim = valors.reduce((a, b) => (b < a ? b : a), valors[0]);
  const diferencia = maxim - minim;

  return { equilibrat: diferencia <= marge, maxim, minim, diferencia };
}

console.log(estaEquilibrat(backlog, 10));
// { equilibrat: false, maxim: 25, minim: 6, diferencia: 19 }

console.log(estaEquilibrat(backlog, 20));
// { equilibrat: true, maxim: 25, minim: 6, diferencia: 19 }

Dos apunts. El patró màxim amb reduce és el mateix de 02-02, i el valor inicial és valors[0] en lloc de 0 perquè amb un 0 fix el mínim sortiria sempre 0. I la comprovació de llista buida és imprescindible: sense ella, valors[0] seria undefined i totes les comparacions donarien false. Aquest tipus de cas límit és exactament el que provaràs sistemàticament al Mòdul 8 amb Jest.

Amb marge = 10 el repartiment no està equilibrat: l'Iván acumula 25 h davant de les 6 h de la Marta. És just el diagnòstic que la Marta necessitava per redistribuir el backlog.

Conclusió

Ja tens l'arsenal complet per consultar una llista. Per cercar: find i findIndex quan vols un element o la seva posició, findLast i findLastIndex quan t'interessa el més recent, some i every quan la resposta és un sí o un no —amb el seu curtcircuit i amb el parany de l'array buit—, i filter quan vols una subllista, recordant que sempre retorna un array i que un array buit és truthy.

Per ordenar, sort amb els seus dos paranys gravats a foc: sense comparadora ordena com a text ([10, 9, 100][10, 100, 9]) i muta l'original. La comparadora retorna negatiu, zero o positiu; a - b per a números, localeCompare per a text en català, comparació directa de strings per a les dates ISO —el premi a aquella decisió de disseny del Mòdul 1—, la resta invertida per a l'ordre descendent i l'encadenat amb || per a diversos criteris. I toSorted/toReversed per no destruir les dades en construir una vista.

Per agregar, reduce, entès de debò: és el patró acumulador amb nom, la seva funció reductora ha de retornar sempre l'acumulador i el seu valor inicial no és opcional. L'has fet servir en els quatre casos que cobreixen el 90 % del que necessitaràs: sumar (48 h, 45 h obertes, esforç 124), comptar per estat, agrupar per responsable (Iván 25 h, Lucía 14 h, Marta 6 h) i construir un índex per id. Al seu costat, Object.groupBy per agrupar sense esforç, Set per desduplicar les 12 etiquetes en 11 d'úniques i Map per a diccionaris amb claus dinàmiques. I saps encadenar filter().toSorted().map() sabent el que costa cada baula. L'informe setmanal de càrrega de Taller Nómada, que era l'entregable pendent des del Mòdul 1, ja està escrit.

Si alguna cosa s'ha repetit de manera sospitosa a les últimes pàgines, són línies com parell[0] i parell[1] en recórrer Object.entries, o const [eliminada] = tasques.splice(...), o const { totals, carrega } = informe. Totes apunten al mateix: hi ha una sintaxi per desempaquetar un array o un objecte en variables amb nom, i fa tres lliçons que la fas servir de gairell sense haver-la estudiat. Comencem per la meitat més senzilla a Desestructuració d'Arrays, on aprendràs a partir una dataLimit en any, mes i dia en una sola línia, a intercanviar variables sense auxiliar i a escriure per fi for (const [i, tasca] of backlog.entries()) entenent exactament què significa.

Curs de JavaScript: De Principiant a Avançat

Mòdul 1: Introducció a JavaScript

Mòdul 2: Estructures de Control

Mòdul 3: Funcions

Mòdul 4: Objectes i Arrays

Mòdul 5: Objectes i Funcions Avançades

Mòdul 6: El Model d'Objectes del Document (DOM)

Mòdul 7: APIs del Navegador i Temes Avançats

Mòdul 8: Proves i Depuració

Mòdul 9: Rendiment i Optimització

Mòdul 10: Frameworks i Llibreries de JavaScript

Mòdul 11: Projecte Final

© Copyright 2026. Tots els drets reservats