La lliçó anterior va acabar amb un límit clar: desempaquetar per posició està molt bé per a parells i ternes, però les tasques de Nómada Tasques tenen nou camps amb nom i allà l'ordre no significa res. Aquesta lliçó cobreix l'altra meitat de la sintaxi —la que mana per nom— i la completa amb l'operador ... en la seva faceta més útil: el spread, que copia i combina objectes i arrays sense tocar els originals. Amb aquestes dues peces escriuràs per fi el patró que sosté el desenvolupament web modern: l'actualització immutable, { ...tasca, estat: 'feta' }, que produeix una tasca modificada sense destruir l'anterior. I acabaràs trobant-te de cara amb el seu límit crític —el spread copia només el primer nivell—, que és exactament el problema que resoldrà l'última lliçó del mòdul.

Contingut

  1. Desestructuració d'objectes: mana el nom
  2. Reanomenar propietats
  3. Valors per defecte (i com combinar-los amb el reanomenament)
  4. Objectes imbricats
  5. Desestructurar als paràmetres: l'objecte d'opcions, ja a la signatura
  6. L'avís: desestructurar undefined i el truc = {}
  7. L'abreviatura de propietat
  8. Spread d'arrays
  9. Spread d'objectes: copiar, combinar, sobreescriure
  10. L'ordre importa
  11. Spread per passar arguments a una funció
  12. Rest en objectes: treure camps
  13. Actualitzacions immutables
  14. Per què això és la base del treball amb estat
  15. El límit crític: el spread copia només la superfície
  16. Errors Habituals i Consells
  17. Exercicis
  18. Conclusió

  1. Desestructuració d'objectes: mana el nom

La sintaxi és idèntica a la d'arrays, però amb claus en lloc de claudàtors:

const tasca = {
  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'
};

const { titol, responsable, horesEstimades } = tasca;
console.log(titol, responsable, horesEstimades);
// Pressupost de la fusteria Iván 5

La diferència essencial amb els arrays:

En un array desempaquetes per posició; en un objecte, per nom de propietat. L'ordre en què escriguis les variables és completament irrellevant.

const { horesEstimades, titol } = tasca;    // ordre diferent, mateix resultat
console.log(titol);      // 'Pressupost de la fusteria'

La contrapartida és que els noms han de coincidir amb els de les propietats. Si t'equivoques, obtens undefined sense cap avís —el mateix problema silenciós de llegir una propietat inexistent que vas veure a 04-01:

const { titlo } = tasca;
console.log(titlo);       // undefined   ← errada, sense error

I si la propietat no existeix, tampoc no falla:

const { comentaris } = tasca;
console.log(comentaris);  // undefined

Comparació de les dues desestructuracions:

Arrays [ ] Objectes { }
Empareja per Posició Nom
Els noms els tries Tu, lliurement Han de coincidir (tret del reanomenament)
Saltar elements Amb comes No cal: demanes només el que vols
Resta ...resta → array ...resta → objecte
Ideal per a Parells, coordenades, entries() Entitats amb camps amb nom

  1. Reanomenar propietats

Si el nom de la propietat no et serveix —perquè xoca amb una variable existent o perquè en vols un de més curt— pots reanomenar amb dos punts:

const { titol: t, horesEstimades: hores, responsable: qui } = tasca;

console.log(t);       // 'Pressupost de la fusteria'
console.log(hores);   // 5
console.log(qui);     // 'Iván'
// console.log(titol);  ✗ ReferenceError: titol no existeix

Es llegeix a l'inrevés del que sembla: titol: t no significa «assigna t a titol», sinó «agafa la propietat titol i anomena-la t». La regla mnemotècnica: a l'esquerra dels dos punts sempre hi ha el nom de la propietat; a la dreta, el nom de la variable. És el mateix ordre que en un literal d'objecte, però funcionant en sentit invers.

El reanomenament és imprescindible quan desestructures dos objectes semblants al mateix àmbit:

const tascaA = { titol: 'Redissenyar la sala polivalent', horesEstimades: 12 };
const tascaB = { titol: 'Pressupost de la fusteria', horesEstimades: 5 };

const { titol: titolA, horesEstimades: horesA } = tascaA;
const { titol: titolB, horesEstimades: horesB } = tascaB;

console.log(`${titolA} (${horesA} h) vs ${titolB} (${horesB} h)`);
// Redissenyar la sala polivalent (12 h) vs Pressupost de la fusteria (5 h)

I també quan la clau no és un identificador vàlid:

const config = { 'mode-fosc': true, 'idioma-per-defecte': 'ca' };
const { 'mode-fosc': modeFosc, 'idioma-per-defecte': idioma } = config;
console.log(modeFosc, idioma);    // true ca

  1. Valors per defecte (i com combinar-los amb el reanomenament)

Igual que amb els arrays, i amb la mateixa regla: el valor per defecte només s'aplica quan el valor és undefined.

const parcial = { titol: 'Revisar la guillotina', horesEstimades: 2 };

const { titol, responsable = 'sense assignar', prioritat = 'mitjana', revisor = null } = parcial;

console.log(responsable);   // 'sense assignar'   ← la propietat no existia
console.log(prioritat);     // 'mitjana'
console.log(revisor);       // null
const ambNul = { revisor: null };
const { revisor = 'sense revisor' } = ambNul;
console.log(revisor);       // null   ← null NO activa el valor per defecte

Aquest comportament és intencionat: null és un valor deliberat («no hi ha revisor i ho sabem»), mentre que undefined significa «no s'ha dit res». Al model de Nómada Tasques, revisor: null és una dada vàlida, així que aquest comportament és el correcte.

Reanomenament i valor per defecte es combinen, posant el valor per defecte després del nou nom:

const { horesEstimades: hores = 0, responsable: qui = 'sense assignar' } = { horesEstimades: 8 };
console.log(hores);   // 8
console.log(qui);     // 'sense assignar'

L'ordre es llegeix així: «agafa horesEstimades, anomena-la hores, i si no ve, que valgui 0».

  1. Objectes imbricats

El patró pot reproduir l'estructura completa de l'objecte:

const projecte = {
  nom: 'Nómada Tasques',
  equip: {
    coordinadora: 'Marta',
    dissenyador: 'Iván',
    desenvolupadora: 'Lucía'
  },
  totals: { hores: 48, obertes: 45 }
};

const { nom, equip: { coordinadora, desenvolupadora }, totals: { hores } } = projecte;

console.log(nom);              // 'Nómada Tasques'
console.log(coordinadora);     // 'Marta'
console.log(desenvolupadora);  // 'Lucía'
console.log(hores);            // 48
// console.log(equip);         ✗ ReferenceError

Detall important que sorprèn tothom: en escriure equip: { coordinadora }, la variable equip no es crea. Els dos punts aquí signifiquen «entra a dins», no «desa això». Si vols les dues coses, demana-les per separat:

const { equip, equip: { coordinadora: cap } } = projecte;
console.log(equip.dissenyador);   // 'Iván'
console.log(cap);                 // 'Marta'

I protegeix sempre els nivells opcionals amb un valor per defecte, o tindràs un TypeError:

const senseEquip = { nom: 'Un altre projecte' };
// const { equip: { coordinadora } } = senseEquip;
// ✗ TypeError: Cannot destructure property 'coordinadora' of 'undefined'

const { equip: { coordinadora = 'sense assignar' } = {} } = senseEquip;
console.log(coordinadora);   // 'sense assignar'   ✓

Com a la lliçó anterior, l'advertiment es manté: si necessites més de dos nivells, la desestructuració deixa d'ajudar. Un projecte.equip.coordinadora directe es llegeix millor.

  1. Desestructurar als paràmetres: l'objecte d'opcions, ja a la signatura

Aquí hi ha l'ús més valuós de tota la lliçó. A 03-03 vas dissenyar crearTasca() rebent un objecte d'opcions, i dins de la funció havies d'anar traient cada camp amb ??. Ara això s'escriu directament a la signatura.

// ABANS (Mòdul 3): la signatura no diu res i el cos s'omple de ??
function crearTascaAntiga(opcions) {
  const titol = opcions.titol;
  const responsable = opcions.responsable ?? 'sense assignar';
  const prioritat = opcions.prioritat ?? 'mitjana';
  const estat = opcions.estat ?? 'pendent';
  const horesEstimades = opcions.horesEstimades ?? 1;
  const revisor = opcions.revisor ?? null;
  // ...
}

// ARA: la signatura és la documentació
function crearTasca({
  id,
  titol,
  responsable = 'sense assignar',
  prioritat = 'mitjana',
  estat = 'pendent',
  etiquetes = [],
  horesEstimades = 1,
  dataLimit,
  revisor = null
}) {
  if (typeof titol !== 'string' || titol.trim() === '') {
    throw new Error('El títol és obligatori.');            // R1, fail-fast de 02-05
  }
  if (!(horesEstimades > 0 && horesEstimades <= 40)) {
    throw new Error('Les hores han de ser entre 0 i 40.');  // R3
  }
  return { id, titol, responsable, prioritat, estat,
           etiquetes, horesEstimades, dataLimit, revisor };
}

const nova = crearTasca({
  id: 7,
  titol: 'Revisar la guillotina per enquadernar',
  responsable: 'Lucía',
  horesEstimades: 2,
  dataLimit: '2026-10-20',
  etiquetes: ['enquadernació', 'manteniment']
});

console.log(nova);
// { id: 7, titol: 'Revisar la guillotina per enquadernar', responsable: 'Lucía',
//   prioritat: 'mitjana', estat: 'pendent',
//   etiquetes: [ 'enquadernació', 'manteniment' ], horesEstimades: 2,
//   dataLimit: '2026-10-20', revisor: null }

Compara les dues versions i fixa't en el que es guanya:

Objecte d'opcions «a mà» Desestructurat a la signatura
Es veuen els camps acceptats? No: cal llegir el cos Sí, a la signatura
Es veuen els valors per defecte? No
Línies de preparació Una per camp Cap
Risc d'errada Alt (opcions.titlo) Igual, però concentrat en un sol lloc

Funciona igual als callbacks dels mètodes de 04-05, i allà la millora és enorme:

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

// Sense desestructurar
const liniesA = backlog.map((t) => `${t.titol} · ${t.responsable} · ${t.horesEstimades} h`);

// Desestructurant al paràmetre: a dins no hi ha ni un sol "t."
const liniesB = backlog.map(({ titol, responsable, horesEstimades }) =>
  `${titol} · ${responsable} · ${horesEstimades} h`
);

console.log(liniesB[5]);   // 'Pressupost de la fusteria · Iván · 5 h'

I en una funció de domini com estaVencuda, la signatura passa a declarar exactament quins camps necessita:

function estaVencuda({ dataLimit, estat }, avui) {
  return dataLimit < avui && estat !== 'feta';          // R10
}

console.log(estaVencuda(backlog[5], '2026-09-20'));   // true

Un matís de criteri: desestructurar a la signatura fa explícita la dependència, però perds l'objecte complet dins de la funció. Si també necessites l'objecte sencer (per exemple per retornar-lo), rep-lo normal i desestructura a la primera línia del cos.

  1. L'avís: desestructurar undefined i el truc = {}

Si crides crearTasca() sense arguments, el programa peta:

// crearTasca();
// ✗ TypeError: Cannot destructure property 'id' of 'undefined' as it is undefined.

No es pot desempaquetar el que no existeix. La solució és donar un valor per defecte al paràmetre sencer:

function resumirOpcions({ ordre = 'data', limit = 10, incloureFetes = false } = {}) {
  return `ordre=${ordre}, limit=${limit}, fetes=${incloureFetes}`;
}

console.log(resumirOpcions());                     // ordre=data, limit=10, fetes=false
console.log(resumirOpcions({ limit: 3 }));         // ordre=data, limit=3, fetes=false
console.log(resumirOpcions({ ordre: 'hores', incloureFetes: true }));
// ordre=hores, limit=10, fetes=true

Aquell = {} final es llegeix: «si no em passen res, desestructura un objecte buit», i aleshores tots els valors per defecte individuals entren en joc. Escriu-lo sempre que tots els camps siguin opcionals; és la diferència entre una funció que es pot cridar sense arguments i una que peta.

El mateix problema apareix fora de les funcions, i la protecció és ?? {}:

const resposta = { dades: undefined };
const { total = 0 } = resposta.dades ?? {};
console.log(total);      // 0   ✓ sense TypeError

  1. L'abreviatura de propietat

Un petit complement que veus des de 04-06. Quan el nom de la variable coincideix amb el de la propietat, no cal repetir-lo:

const id = 7;
const titol = 'Revisar la guillotina';
const responsable = 'Lucía';

// Forma llarga
const tascaLlarga = { id: id, titol: titol, responsable: responsable };

// Abreviatura de propietat: idèntica, però sense repetir
const tasca = { id, titol, responsable };

console.log(tasca);   // { id: 7, titol: 'Revisar la guillotina', responsable: 'Lucía' }

És l'operació inversa de la desestructuració: aquesta empaqueta, aquella desempaqueta. I es combinen de manera natural en el patró «rebre, transformar, retornar»:

function normalitzarTasca({ titol, responsable, etiquetes = [] }) {
  const titolNet = titol.trim();
  const etiquetesNetes = etiquetes.map((e) => e.trim().toLowerCase());   // R9
  return { titol: titolNet, responsable, etiquetes: etiquetesNetes };
}

console.log(normalitzarTasca({ titol: '  Pressupost de la fusteria ',
                               responsable: 'Iván',
                               etiquetes: [' Fusteria', 'COMPRES'] }));
// { titol: 'Pressupost de la fusteria', responsable: 'Iván',
//   etiquetes: [ 'fusteria', 'compres' ] }

Fixa't en responsable a seques a l'objecte retornat: és l'abreviatura, perquè el valor no canvia.

  1. Spread d'arrays

L'operador ... té dues cares. Al costat esquerre d'una assignació o als paràmetres recull (és rest, el que ja coneixes). Al costat dret, dins d'un literal o d'una crida, desplega: és el spread.

const deIvan = ['Redissenyar la sala polivalent', 'Guia sobre enquadernació'];
const deMarta = ['Cartelleria del taller de serigrafia'];

// Copiar
const copia = [...deIvan];
console.log(copia);              // [ 'Redissenyar la sala polivalent', 'Guia sobre enquadernació' ]
console.log(copia === deIvan);   // false   ← és un array nou

// Combinar (substitueix concat)
const totes = [...deIvan, ...deMarta];
console.log(totes.length);       // 3

// Afegir elements al principi, al mig o al final
const ambUrgent = ['URGENT', ...deIvan];
const alMig = [deIvan[0], 'Nova tasca intercalada', ...deIvan.slice(1)];
console.log(alMig.length);       // 3

// Funciona amb qualsevol iterable
console.log([...'Nómada']);      // [ 'N', 'ó', 'm', 'a', 'd', 'a' ]
console.log([...new Set([1, 1, 2])]);   // [ 1, 2 ]

Aquest últim exemple és l'idioma curt per desduplicar que vam anunciar a 04-05: [...new Set(array)] equival a Array.from(new Set(array)).

I aquesta és la manera moderna d'escriure les operacions immutables sobre llistes que a 04-03 feies amb slice i concat:

const etiquetes = ['fusteria', 'compres'];

const ambNova = [...etiquetes, 'urgent'];                 // afegir al final
const sensePrimera = etiquetes.slice(1);                  // treure la primera
const inserida = [...etiquetes.slice(0, 1), 'pressupostos', ...etiquetes.slice(1)];

console.log(ambNova);       // [ 'fusteria', 'compres', 'urgent' ]
console.log(inserida);      // [ 'fusteria', 'pressupostos', 'compres' ]
console.log(etiquetes);     // [ 'fusteria', 'compres' ]   ✓ intacte

  1. Spread d'objectes: copiar, combinar, sobreescriure

El mateix, amb claus. El spread d'objectes copia les propietats pròpies i enumerables dins d'un objecte nou.

const tasca = backlog[5];

// Copiar
const copia = { ...tasca };
console.log(copia.titol);          // 'Pressupost de la fusteria'
console.log(copia === tasca);      // false   ← objecte nou

// Combinar dos objectes
const valorsPerDefecte = { prioritat: 'mitjana', estat: 'pendent', revisor: null, etiquetes: [] };
const dadesDelFormulari = { id: 8, titol: 'Comprar tinta negra', responsable: 'Marta' };

const nova = { ...valorsPerDefecte, ...dadesDelFormulari };
console.log(nova);
// { prioritat: 'mitjana', estat: 'pendent', revisor: null, etiquetes: [],
//   id: 8, titol: 'Comprar tinta negra', responsable: 'Marta' }

// Sobreescriure un camp concret
const acabada = { ...tasca, estat: 'feta' };
console.log(acabada.estat);        // 'feta'
console.log(tasca.estat);          // 'pendent'   ✓ l'original, intacte

Aquest últim bloc és el patró que dóna nom a la lliçó, i el desenvoluparem a l'apartat 13.

També existeix Object.assign(desti, ...fonts), que feia el mateix abans que el spread existís. Amb una diferència perillosa:

// Object.assign MUTA el primer argument
const desti = { a: 1 };
Object.assign(desti, { b: 2 });
console.log(desti);                // { a: 1, b: 2 }   ← ha canviat

// Tret que li passis un objecte buit com a destinació
const segur = Object.assign({}, { a: 1 }, { b: 2 });   // equival a { ...{a:1}, ...{b:2} }

En codi nou, fes servir el spread: fa el mateix, no muta res per accident i es llegeix millor.

  1. L'ordre importa

Quan dos objectes tenen la mateixa clau, guanya l'últim. Aquesta regla és la que fa útil el spread, i la que produeix els bugs quan s'oblida.

const base = { prioritat: 'mitjana', estat: 'pendent' };
const canvis = { prioritat: 'alta' };

console.log({ ...base, ...canvis });   // { prioritat: 'alta', estat: 'pendent' }     ✓
console.log({ ...canvis, ...base });   // { prioritat: 'mitjana', estat: 'pendent' }  ✗

A la segona línia, base s'aplica després i esclafa el canvi. La regla mnemotècnica és directa: els valors per defecte van primer, les dades reals després.

function ambValorsPerDefecte(dades) {
  return { prioritat: 'mitjana', estat: 'pendent', revisor: null, etiquetes: [], ...dades };
  //       ↑ valors per defecte primer                                          ↑ les dades guanyen
}

console.log(ambValorsPerDefecte({ titol: 'Comprar tinta', prioritat: 'alta' }));
// { prioritat: 'alta', estat: 'pendent', revisor: null, etiquetes: [], titol: 'Comprar tinta' }

Un detall a tenir present: el spread copia també les propietats el valor de les quals és undefined, i aquestes esclafen el valor per defecte.

console.log(ambValorsPerDefecte({ prioritat: undefined }));
// { prioritat: undefined, ... }   ✗ el valor per defecte s'ha perdut

Si això et preocupa, filtra els undefined abans o fes servir desestructuració amb valors per defecte, que sí que els ignora.

  1. Spread per passar arguments a una funció

El spread desplega un array en arguments solts, substituint apply (04-02):

const hores = [12, 6, 14, 3, 8, 5];

console.log(Math.max(...hores));    // 14
console.log(Math.min(...hores));    // 3

// Abans d'ES2015 calia escriure:
console.log(Math.max.apply(null, hores));   // 14

És la contrapartida exacta del paràmetre ...rest que vas estudiar a 03-03:

function registrar(accio, ...detalls) {       // rest: RECULL els arguments en un array
  console.log(`${accio}: ${detalls.join(' / ')}`);
}

const dades = ['Iván', 'alta', '5 h'];
registrar('Nova tasca', ...dades);            // spread: DESPLEGA l'array en arguments
// Nova tasca: Iván / alta / 5 h
On apareix ... Com s'anomena Què fa
Als paràmetres d'una funció Rest Recull els arguments sobrants en un array
En un patró de desestructuració Rest Recull el que quedi
Dins d'un literal [ ] o { } Spread Desplega el contingut
En una crida a funció Spread Desplega l'array en arguments

Regla mnemotècnica: a l'esquerra de l'= recull, a la dreta reparteix.

  1. Rest en objectes: treure camps

En un patró d'objecte, ...resta recull en un objecte nou totes les propietats que no hagis anomenat. És la manera idiomàtica de treure camps sense delete:

const tasca = backlog[0];

const { id, ...senseId } = tasca;
console.log(id);                     // 1
console.log(Object.keys(senseId));
// [ 'titol', 'responsable', 'prioritat', 'estat', 'horesEstimades', 'dataLimit' ]

// Treure'n diversos alhora
const { id: _id, dataLimit: _d, ...perAlInforme } = tasca;
console.log(Object.keys(perAlInforme));
// [ 'titol', 'responsable', 'prioritat', 'estat', 'horesEstimades' ]

Com amb els arrays, la resta ha d'anar l'última i sempre produeix un objecte (buit si no queda res).

Un cas molt real: preparar les dades que s'envien a algun lloc, sense camps interns.

function perExportar(tasca) {
  const { revisor, etiquetes, ...publica } = tasca;
  return publica;
}

I compara-ho amb delete, que ja coneixes de 04-01:

// ✗ Muta l'objecte original: qui el tingués es queda sense camp
delete tasca.revisor;

// ✓ Crea un objecte nou sense aquell camp; l'original no es toca
const { revisor, ...senseRevisor } = tasca;

  1. Actualitzacions immutables

Ara sí, el patró central. La pregunta és: com es canvia l'estat d'una tasca?

// Opció A: MUTAR
function marcarFetaMutant(tasca) {
  tasca.estat = 'feta';
  return tasca;
}

// Opció B: crear una tasca NOVA amb el camp canviat
function marcarFeta(tasca) {
  return { ...tasca, estat: 'feta' };
}

const original = backlog[5];
const actualitzada = marcarFeta(original);

console.log(actualitzada.estat);   // 'feta'
console.log(original.estat);       // 'pendent'   ✓ l'original continua igual
console.log(actualitzada === original);   // false
flowchart LR
    A["tasca original<br/>estat: 'pendent'"] -->|"{ ...tasca, estat: 'feta' }"| B["tasca NOVA<br/>estat: 'feta'"]
    A -.->|"continua existint,<br/>sense canvis"| A2["tasca original<br/>estat: 'pendent'"]

El mateix patró s'estén a la llista completa, combinant spread amb map (04-04):

/** Retorna un backlog NOU amb la tasca `id` actualitzada. */
function actualitzarTasca(tasques, id, canvis) {
  return tasques.map((tasca) => (tasca.id === id ? { ...tasca, ...canvis } : tasca));
}

const backlogV2 = actualitzarTasca(backlog, 6, { estat: 'feta', revisor: 'Marta' });

console.log(backlogV2[5].estat);    // 'feta'
console.log(backlog[5].estat);      // 'pendent'   ✓ intacte
console.log(backlogV2 === backlog); // false
console.log(backlogV2[0] === backlog[0]);   // true  ← les NO modificades es reaprofiten

Aquesta última línia és una propietat valuosa: les tasques que no canvien són literalment el mateix objecte, així que comparar amb === et diu a l'instant què ha canviat sense recórrer res. És el fonament de les optimitzacions de renderitzat que veuràs al Mòdul 10.

I el joc complet d'operacions immutables sobre el backlog:

const afegir = (tasques, tasca) => [...tasques, tasca];
const eliminar = (tasques, id) => tasques.filter((t) => t.id !== id);
const actualitzar = (tasques, id, canvis) =>
  tasques.map((t) => (t.id === id ? { ...t, ...canvis } : t));
const reordenar = (tasques, cmp) => tasques.toSorted(cmp);

const v1 = afegir(backlog, { id: 7, titol: 'Revisar la guillotina', responsable: 'Lucía',
                             prioritat: 'mitjana', estat: 'pendent', horesEstimades: 2,
                             dataLimit: '2026-10-20' });
const v2 = actualitzar(v1, 6, { estat: 'feta' });
const v3 = eliminar(v2, 4);

console.log(backlog.length, v1.length, v2.length, v3.length);   // 6 7 7 6

Quatre versions del backlog convivint, i cap no ha destruït l'anterior. Desfer un canvi és tan simple com quedar-se amb la versió prèvia.

  1. Per què això és la base del treball amb estat

Pot semblar un caprici crear objectes nous en lloc de modificar els que hi ha. No ho és, i convé entendre per què abans d'arribar al Mòdul 10.

Avantatge Què significa a la pràctica
Detectar canvis és trivial anterior === nou respon a l'instant, sense comparar camp a camp
Historial de franc Desant les versions anteriors tens desfer/refer sense esforç
Sense efectes a distància Ningú no modifica per sorpresa un objecte que tu tenies desat
Depuració predictible Pots inspeccionar l'estat a cada pas perquè no se sobreescriu
Concurrència segura Cap dada no canvia sota els peus d'una operació en curs (Mòdul 5)

Els frameworks moderns estan construïts sobre aquesta idea: a React, actualitzar l'estat significa retornar un objecte nou, no modificar el que hi havia; a Redux, el reductor és literalment una funció que rep l'estat i en retorna un de nou —i sí, s'anomena reductor per la mateixa raó que reduce—. Ho veuràs amb detall a Per Què Existeixen els Frameworks i Redux. El que importa ara és que la sintaxi que acabes d'aprendre, { ...tasca, estat: 'feta' }, és exactament la que escriuràs allà.

El cost, per ser honestos: crear còpies consumeix memòria i temps. Amb sis tasques és irrellevant; amb llistes enormes hi ha tècniques específiques, i és tema del Mòdul 9. La recomanació pràctica continua sent la mateixa: immutable per defecte, i muta només dins d'una funció, sobre dades que aquella funció ha creat i que ningú més no veu —com vas fer al reduce del rànquing de càrrega a 04-05.

  1. El límit crític: el spread copia només la superfície

I aquí arriba el problema que aquesta lliçó no pot resoldre. Prova això:

const tasca = {
  id: 1,
  titol: 'Redissenyar la sala polivalent',
  etiquetes: ['espai', 'disseny']
};

const copia = { ...tasca };

// Canviar un camp simple: perfecte, són independents
copia.titol = 'Un altre títol';
console.log(tasca.titol);       // 'Redissenyar la sala polivalent'  ✓

// Canviar alguna cosa DINS de l'array: desastre
copia.etiquetes.push('mobiliari');
console.log(tasca.etiquetes);   // [ 'espai', 'disseny', 'mobiliari' ]  ✗ l'original ha canviat!
console.log(copia.etiquetes === tasca.etiquetes);   // true  ← és el MATEIX array

L'explicació ve de 01-05: els valors primitius es copien; els objectes i arrays es copien per referència. El spread copia els valors de les propietats, i el valor d'etiquetes és una referència. Resultat: dos objectes diferents que apunten al mateix array.

flowchart TD
    A["tasca<br/>{ id, titol, etiquetes }"] --> C["array<br/>['espai', 'disseny']"]
    B["copia<br/>{ id, titol, etiquetes }"] --> C
    C -.->|"push des de qualsevol<br/>afecta els dos"| C

Això s'anomena còpia superficial (shallow copy): copia un nivell. Per als camps primitius d'una tasca —id, titol, estat, horesEstimades, dataLimit— n'hi ha prou. Per a etiquetes i per a les subtasques imbricades, no.

Hi ha un apedaçament manual, que consisteix a desplegar també el nivell interior:

const copiaBona = { ...tasca, etiquetes: [...tasca.etiquetes] };
copiaBona.etiquetes.push('mobiliari');
console.log(tasca.etiquetes);   // [ 'espai', 'disseny' ]   ✓ fora de perill

Funciona, però exigeix conèixer i escriure a mà cada nivell imbricat. Amb l'arbre de subtasques de 03-07, que pot tenir qualsevol profunditat, és directament inviable. Necessites una còpia profunda, i les eines per aconseguir-la —structuredClone, el vell truc amb JSON i les seves limitacions— són el contingut de JSON i Còpies d'Objectes.

Errors Habituals i Consells

1. Llegir el reanomenament a l'inrevés. { titol: t } significa «la propietat titol s'anomenarà t», no al contrari.

2. Creure que en desestructurar un objecte imbricat es crea la variable intermèdia. const { equip: { cap } } = p; no crea equip.

3. Oblidar el = {} en un paràmetre desestructurat. Cridar la funció sense arguments llança un TypeError.

4. Posar les dades abans dels valors per defecte en combinar amb spread. Els valors per defecte van primer; el que guanya és l'últim.

5. Fer servir Object.assign(obj, canvis) creient que copia. Muta el primer argument. Fes servir { ...obj, ...canvis }.

6. Posar la resta al mig. const { ...resta, id } = tasca; és un SyntaxError.

7. Confiar que el spread fa una còpia completa. Copia un sol nivell: els arrays i objectes imbricats es comparteixen.

8. Desestructurar vint camps perquè sí. Si la funció en fa servir tres, demana'n tres. Una signatura amb quinze noms és pitjor que (tasca).

Consell professional. Fes servir la desestructuració per declarar el que necessites i el spread per declarar el que canvies. function estaVencuda({ dataLimit, estat }, avui) diu, sense comentaris, que la regla R10 només depèn d'aquests dos camps; i { ...tasca, estat: 'feta' } diu, sense comentaris, que l'única cosa que canvia és l'estat. Aquest estil declaratiu és la meitat del valor d'aquesta sintaxi; l'altra meitat és no trencar res pel camí.

Exercicis

Exercici 1 — Signatura que es documenta sola. Reescriu aquestes dues funcions desestructurant als paràmetres, amb valors per defecte on tingui sentit, i de manera que es puguin cridar sense arguments sense llançar error:

function resumDeTasca(tasca) {
  return tasca.titol + ' (' + tasca.responsable + ', ' + tasca.horesEstimades + ' h)';
}

function filtrarBacklog(tasques, opcions) {
  const estat = opcions.estat;
  const minimHores = opcions.minimHores || 0;
  return tasques.filter((t) => (!estat || t.estat === estat) && t.horesEstimades >= minimHores);
}

Exercici 2 — Canvis d'estat sense danys. Escriu aquestes tres funcions, totes immutables:

  1. canviarEstat(tasques, id, estatNou) — retorna un backlog nou amb aquella tasca a l'estat indicat; si l'estat no és vàlid, llança un Error.
  2. assignarRevisor(tasques, id, revisor) — igual, però canviant el revisor.
  3. senseCampsInterns(tasca) — retorna la tasca sense revisor ni etiquetes, fent servir rest.

Comprova després de cadascuna que el backlog original no ha canviat.

Exercici 3 — Fusió de plantilles. Taller Nómada té plantilles de tasca per tipus d'encàrrec. Escriu desDePlantilla(plantilla, dades) que combini tres capes: uns valors per defecte globals, la plantilla i les dades concretes, en l'ordre correcte de precedència. Fes servir aquesta plantilla i aquestes dades:

const PLANTILLA_SERIGRAFIA = { prioritat: 'mitjana', etiquetes: ['serigrafia'], horesEstimades: 4, revisor: 'Marta' };
const dades = { id: 9, titol: 'Samarretes del curs de tardor', responsable: 'Iván', horesEstimades: 6 };

Comprova a més que modificar les etiquetes del resultat no modifica les de la plantilla, i explica què has hagut de fer per aconseguir-ho.

Solucions

Exercici 1

function resumDeTasca({ titol = 'sense títol', responsable = 'sense assignar', horesEstimades = 0 } = {}) {
  return `${titol} (${responsable}, ${horesEstimades} h)`;
}

function filtrarBacklog(tasques = [], { estat, minimHores = 0 } = {}) {
  return tasques.filter((t) => (!estat || t.estat === estat) && t.horesEstimades >= minimHores);
}

console.log(resumDeTasca(backlog[2]));
// 'Actualitzar el web de reserves (Lucía, 14 h)'
console.log(resumDeTasca());
// 'sense títol (sense assignar, 0 h)'

console.log(filtrarBacklog(backlog, { estat: 'pendent' }).length);           // 3
console.log(filtrarBacklog(backlog, { minimHores: 10 }).map((t) => t.id));   // [ 1, 3 ]
console.log(filtrarBacklog(backlog).length);                                 // 6
console.log(filtrarBacklog().length);                                        // 0

Els dos = {} són la clau perquè les últimes crides no petin. Fixa't també que estat no porta valor per defecte: aquí undefined significa «no filtris per estat», i això és exactament el que comprova !estat. Posar-hi un valor per defecte hauria destruït aquesta semàntica.

Exercici 2

const ESTATS_VALIDS = ['pendent', 'en-curs', 'feta'];

function canviarEstat(tasques, id, estatNou) {
  if (!ESTATS_VALIDS.includes(estatNou)) {
    throw new Error(`Estat no vàlid: ${estatNou}`);
  }
  if (!tasques.some((t) => t.id === id)) {
    throw new Error(`No hi ha cap tasca amb id ${id}.`);
  }
  return tasques.map((t) => (t.id === id ? { ...t, estat: estatNou } : t));
}

function assignarRevisor(tasques, id, revisor) {
  return tasques.map((t) => (t.id === id ? { ...t, revisor } : t));
}

function senseCampsInterns(tasca) {
  const { revisor, etiquetes, ...publica } = tasca;
  return publica;
}

const backlogV2 = canviarEstat(backlog, 6, 'feta');
console.log(backlogV2[5].estat);       // 'feta'
console.log(backlog[5].estat);         // 'pendent'   ✓

const backlogV3 = assignarRevisor(backlogV2, 2, 'Lucía');
console.log(backlogV3[1].revisor);     // 'Lucía'
console.log(backlogV2[1].revisor);     // undefined  ✓ la versió anterior no s'ha tocat

console.log(Object.keys(senseCampsInterns(backlog[0])));
// [ 'id', 'titol', 'responsable', 'prioritat', 'estat', 'horesEstimades', 'dataLimit' ]

try {
  canviarEstat(backlog, 1, 'acabada');
} catch (error) {
  console.log(error.message);          // 'Estat no vàlid: acabada'
}

Tres detalls. A assignarRevisor, { ...t, revisor } fa servir l'abreviatura de propietat: la variable s'anomena igual que el camp. Les validacions van abans de construir res, seguint el principi de fail-fast de 02-05. I some (04-05) és la manera correcta de comprovar l'existència sense recórrer la llista sencera.

Exercici 3

const PER_DEFECTE = {
  prioritat: 'mitjana',
  estat: 'pendent',
  etiquetes: [],
  horesEstimades: 1,
  revisor: null
};

function desDePlantilla(plantilla = {}, dades = {}) {
  return {
    ...PER_DEFECTE,                               // 1. el més genèric
    ...plantilla,                                 // 2. la plantilla trepitja els valors per defecte
    ...dades,                                     // 3. les dades concretes manen
    etiquetes: [                                  // 4. les etiquetes s'acumulen, no es trepitgen
      ...new Set([...(plantilla.etiquetes ?? []), ...(dades.etiquetes ?? [])])
    ]
  };
}

const PLANTILLA_SERIGRAFIA = { prioritat: 'mitjana', etiquetes: ['serigrafia'], horesEstimades: 4, revisor: 'Marta' };
const dades = { id: 9, titol: 'Samarretes del curs de tardor', responsable: 'Iván', horesEstimades: 6 };

const nova = desDePlantilla(PLANTILLA_SERIGRAFIA, dades);
console.log(nova);
// { prioritat: 'mitjana', estat: 'pendent', horesEstimades: 6, revisor: 'Marta',
//   id: 9, titol: 'Samarretes del curs de tardor', responsable: 'Iván',
//   etiquetes: [ 'serigrafia' ] }

nova.etiquetes.push('samarretes');
console.log(PLANTILLA_SERIGRAFIA.etiquetes);   // [ 'serigrafia' ]   ✓ intacta

L'essencial de l'exercici és al punt 4. Si la funció s'hagués limitat a { ...PER_DEFECTE, ...plantilla, ...dades }, el camp etiquetes del resultat seria el mateix array que el de PLANTILLA_SERIGRAFIA, i el push final hauria contaminat la plantilla per a totes les tasques futures: el problema de la còpia superficial de l'apartat 15. La solució ha estat desplegar explícitament l'array interior amb [...] —creant-ne un de nou— i, de passada, fer servir un Set per no duplicar etiquetes que vinguin per les dues vies. Aquesta feina manual, nivell per nivell, és exactament el que la lliçó següent automatitza.

Observa també que horesEstimades acaba sent 6 i no 4: les dades concretes van les últimes i guanyen, tal com diu la regla de l'ordre.

Conclusió

Tens la sintaxi completa. La desestructuració d'objectes empareja per nom: pots reanomenar amb { titol: t } —llegint-ho sempre com «propietat a l'esquerra, variable a la dreta»—, donar valors per defecte que només s'activen amb undefined, combinar totes dues coses, i entrar en objectes imbricats recordant que la variable intermèdia no es crea. El seu millor ús són els paràmetres: l'objecte d'opcions que a 03-03 es desmuntava línia a línia dins del cos ara es declara a la signatura, amb crearTasca({ ... }) i estaVencuda({ dataLimit, estat }, avui) documentant-se soles, i sempre amb el = {} que evita el TypeError en cridar sense arguments.

De l'altre costat, l'operador ... en les seves dues cares: rest quan recull (paràmetres sobrants, propietats sense anomenar, const { id, ...resta } = tasca per treure camps sense delete) i spread quan reparteix (copiar, combinar, sobreescriure, inserir al mig, passar arguments amb Math.max(...hores)). Amb la regla que ho governa tot: en combinar, guanya l'últim, així que els valors per defecte van primer i les dades reals després. I amb això has escrit el patró que sosté el desenvolupament modern: l'actualització immutable { ...tasca, estat: 'feta' } i la seva versió sobre llistes, tasques.map((t) => t.id === id ? { ...t, ...canvis } : t), que produeix un backlog nou conservant per referència les tasques que no han canviat.

Però has acabat topant amb un mur. El spread fa una còpia superficial: canviar copia.titol és segur, però copia.etiquetes.push(...) modifica també l'original, perquè tots dos objectes apunten al mateix array. Hi ha un apedaçament manual, { ...tasca, etiquetes: [...tasca.etiquetes] }, i funciona mentre coneguis tots els nivells imbricats; amb l'arbre de subtasques de profunditat desconeguda, no hi ha pedaç que valgui. Necessites una còpia profunda de debò. Això, juntament amb l'altra gran pregunta que encara no has resolt —com es converteix el backlog en text per poder-lo desar i recuperar— és el contingut de JSON i Còpies d'Objectes, l'última lliçó del mòdul.

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