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
- El punt de partida: el backlog canònic
- Cercar un element:
find,findIndex,findLast - Comprovar el conjunt:
someievery - Seleccionar-ne uns quants:
filter - La taula de la cerca
sort: el parany dels números- La funció comparadora
- Ordenar per número, per text i per data
- Ordre descendent i ordenació per diversos criteris
toSorteditoReversed: ordenar sense mutarreduce, a poc a poc- Quatre casos reals de
reduce Object.groupBy,MapiSet- Encadenar
filter().map().sort()i el seu cost - Exemple integrador: l'informe setmanal de càrrega
- Errors Habituals i Consells
- Exercicis
- Conclusió
- 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.
- Cercar un element:
find, findIndex, findLast
find, findIndex, findLastfind(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); // undefinedCompara-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 altaRecorda 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 ✓
- Comprovar el conjunt:
some i every
some i everyDe 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 compleixenAquest 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
- Seleccionar-ne uns quants:
filter
filterfilter(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 undefinedAquest ú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);
- 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 |
Sí | Cerques una tasca concreta |
findIndex(p) |
La seva posició, o -1 |
Sí | 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 |
Sí | Cerques un primitiu exacte |
indexOf(v) |
Posició o -1 |
Sí | Cerques la posició d'un primitiu |
sort: el parany dels números
sort: el parany dels númerosComencem 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, destrossatDues regles que se'n deriven i que has d'aplicar sempre:
- A
sortse li passa sempre una funció comparadora. Sense excepcions, ni tan sols amb números. - Copia abans d'ordenar, tret que vulguis mutar de debò:
array.slice().sort(cmp)o, millor,array.toSorted(cmp).
- 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.
- 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 14hPer 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.
- 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); // 14Quan 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.
toSorted i toReversed: ordenar sense mutar
toSorted i toReversed: ordenar sense mutarEls 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);
reduce, a poc a poc
reduce, a poc a pocreduce és el mètode més potent de l'arsenal i el que pitjor s'explica. La idea, en una frase:
reducerecorre 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:
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); // 48La 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 sumaI 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)); // ✓ 0Regla sense excepcions: posa sempre el valor inicial. I tria'l amb cura:
0per sumar,1per 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; }, []);
- Quatre casos reals de
reduce
reduceCas 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); // 45Cas 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); // undefinedPer 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.
Object.groupBy, Map i Set
Object.groupBy, Map i SetObject.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 |
- Encadenar
filter().map().sort() i el seu cost
filter().map().sort() i el seu costCom 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.
- 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ó:
- La tasca amb
id5. - Hi ha alguna tasca de la Lucía sense revisor?
- Totes les tasques obertes tenen almenys una etiqueta?
- Els títols de les tasques de prioritat mitjana.
- 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
- Què és JavaScript?
- Configuració del teu Entorn de Desenvolupament
- El teu Primer Programa en JavaScript
- Sintaxi i Conceptes Bàsics de JavaScript
- Variables i Tipus de Dades
- Operadors Bàsics
- Conversió de Tipus i Comparacions
- El Projecte del Curs: Nómada Tasques
Mòdul 2: Estructures de Control
- Sentències Condicionals
- Bucles: for, while, do-while
- Sentències Switch
- Control del Flux: break, continue i Bucles Imbricats
- Gestió d'Errors amb try-catch
Mòdul 3: Funcions
- Definició i Crida de Funcions
- Expressions de Funció i Funcions Fletxa
- Paràmetres i Valors de Retorn
- Àmbit i Closures
- Hoisting i el Context d'Execució
- Funcions d'Ordre Superior
- Recursivitat
Mòdul 4: Objectes i Arrays
- Introducció als Objectes
- Mètodes d'Objecte i la Paraula Clau
this - Arrays: Conceptes Bàsics i Mètodes
- Iteració sobre Arrays
- Cercar, Ordenar i Agregar Dades: find, sort i reduce
- Desestructuració d'Arrays
- Desestructuració d'Objectes, Spread i Rest
- JSON i Còpies d'Objectes
Mòdul 5: Objectes i Funcions Avançades
- Prototips i Herència
- Classes i Programació Orientada a Objectes
- Encapsulació: Getters, Setters i Camps Privats
- Mòduls i Importació/Exportació
- JavaScript Asíncron: Callbacks
- Promeses i Async/Await
- El Bucle d'Esdeveniments i la Cua de Microtasques
- Iteradors i Generadors
Mòdul 6: El Model d'Objectes del Document (DOM)
- Introducció al DOM
- Selecció i Manipulació d'Elements del DOM
- Gestió d'Esdeveniments
- Propagació, Delegació i Esdeveniments Personalitzats
- Creació i Eliminació d'Elements del DOM
- Renderitzat de Llistes i Plantilles HTML
- Gestió i Validació de Formularis
Mòdul 7: APIs del Navegador i Temes Avançats
- Emmagatzematge Local i de Sessió
- Fetch API i AJAX
- Peticions Robustes: Errors, Timeouts i AbortController
- WebSockets
- Service Workers i Aplicacions Web Progressives (PWAs)
- APIs del Navegador Essencials
- Introducció a WebAssembly
Mòdul 8: Proves i Depuració
- Depuració de JavaScript
- Qualitat de Codi: ESLint, Prettier i Convencions
- Proves Unitàries amb Jest
- Dobles de Prova: Mocks, Stubs i Spies
- Proves d'Integració
- Proves d'Extrem a Extrem amb Cypress
Mòdul 9: Rendiment i Optimització
- Mesurar Abans d'Optimitzar: DevTools i Web Vitals
- Optimització del Rendiment de JavaScript
- Gestió de Memòria
- Manipulació Eficient del DOM
- Càrrega Diferida i Divisió de Codi
Mòdul 10: Frameworks i Llibreries de JavaScript
- Per Què Existeixen els Frameworks
- Introducció a React
- Gestió d'Estat amb Redux
- Conceptes Bàsics de Vue.js
- Conceptes Bàsics d'Angular
- Triar el Framework Adequat
