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
- Desestructuració d'objectes: mana el nom
- Reanomenar propietats
- Valors per defecte (i com combinar-los amb el reanomenament)
- Objectes imbricats
- Desestructurar als paràmetres: l'objecte d'opcions, ja a la signatura
- L'avís: desestructurar
undefinedi el truc= {} - L'abreviatura de propietat
- Spread d'arrays
- Spread d'objectes: copiar, combinar, sobreescriure
- L'ordre importa
- Spread per passar arguments a una funció
- Rest en objectes: treure camps
- Actualitzacions immutables
- Per què això és la base del treball amb estat
- El límit crític: el spread copia només la superfície
- Errors Habituals i Consells
- Exercicis
- Conclusió
- 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 5La 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:
I si la propietat no existeix, tampoc no falla:
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 |
- 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 existeixEs 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
- 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); // nullconst ambNul = { revisor: null };
const { revisor = 'sense revisor' } = ambNul;
console.log(revisor); // null ← null NO activa el valor per defecteAquest 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».
- 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); ✗ ReferenceErrorDetall 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.
- 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 | Sí |
| 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')); // trueUn 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.
- L'avís: desestructurar
undefined i el truc = {}
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=trueAquell = {} 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
- 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.
- 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
- 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, intacteAquest ú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.
- 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 perdutSi això et preocupa, filtra els undefined abans o fes servir desestructuració amb valors per defecte, que sí que els ignora.
- 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 hOn 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.
- 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.
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;
- 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); // falseflowchart 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 reaprofitenAquesta ú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 6Quatre 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.
- 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.
- 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 arrayL'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 perillFunciona, 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:
canviarEstat(tasques, id, estatNou)— retorna un backlog nou amb aquella tasca a l'estat indicat; si l'estat no és vàlid, llança unError.assignarRevisor(tasques, id, revisor)— igual, però canviant el revisor.senseCampsInterns(tasca)— retorna la tasca senserevisornietiquetes, 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); // 0Els 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' ] ✓ intactaL'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
- 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
