La lliçó anterior va acabar topant amb un mur: el spread copia només el primer nivell, així que copia.etiquetes.push('mobiliari') modifica també l'original. I va quedar oberta una altra pregunta igual d'important: el backlog de Nómada Tasques viu a la memòria i desapareix tan bon punt es tanca la pestanya; per desar-lo o enviar-lo cal convertir-lo en text. Les dues qüestions es responen amb les mateixes eines, i aquest és el tema d'aquesta lliçó: JSON com a format d'intercanvi, amb JSON.stringify i JSON.parse i tot el que es perd en el viatge d'anada i tornada; i les còpies, superficials i profundes, amb structuredClone com a resposta moderna i Object.freeze com a xarxa de seguretat. És l'última lliçó del mòdul, així que també tancarem el cercle: veuràs què et falta encara perquè cada tasca deixi de repetir els mateixos mètodes una vegada i una altra.

Contingut

  1. Què és JSON
  2. JSON no és JavaScript: la taula de diferències
  3. JSON.stringify: convertir a text
  4. Donar format amb el tercer paràmetre
  5. Filtrar amb el replacer i amb l'array de claus
  6. toJSON: que un objecte decideixi com es serialitza
  7. Què es perd en el viatge d'anada i tornada
  8. JSON.parse: tornar a convertir en objecte
  9. El reviver: transformar mentre es llegeix
  10. JSON.parse sempre dins d'un try/catch
  11. Còpia superficial: { ...obj } i Object.assign
  12. Còpia profunda: structuredClone i el truc amb JSON
  13. La taula de les còpies
  14. Referències compartides: el bug del backlog
  15. Object.freeze i la seva superficialitat
  16. Exemple integrat: desar i recuperar el backlog
  17. Errors Habituals i Consells
  18. Exercicis
  19. Conclusió

  1. Què és JSON

JSON (JavaScript Object Notation) és un format de text per representar dades estructurades. Va néixer de la sintaxi dels objectes de JavaScript, però avui és un estàndard independent que entenen pràcticament tots els llenguatges: quan una aplicació web demana dades a un servidor (Mòdul 7), el que viatja per la xarxa és gairebé sempre JSON.

Un objecte JavaScript i la seva representació JSON, un al costat de l'altre:

// Objecte JavaScript, un valor viu a la memòria
const tasca = { id: 6, titol: 'Pressupost de la fusteria', horesEstimades: 5 };
{"id":6,"titol":"Pressupost de la fusteria","horesEstimades":5}

Aquest segon bloc és un string. No té propietats, no se li pot fer tasca.titol, no ocupa memòria com a objecte: és una seqüència de caràcters que descriu un objecte. La diferència sembla òbvia i és la font del malentès número u amb JSON.

JSON és text. Un objecte es «serialitza» a JSON per poder-lo desar, enviar o comparar; i es «parseja» de tornada per tornar a treballar-hi.

Els tipus que JSON admet són només sis: string, number, boolean, null, objecte i array. Res més.

  1. JSON no és JavaScript: la taula de diferències

La sintaxi s'assembla, però JSON és molt més estricte. Aquestes són les diferències que provoquen errors reals:

Aspecte Objecte JavaScript JSON
Cometes a les claus Opcionals Obligatòries, i dobles: {"id": 6}
Cometes als strings Simples o dobles Només dobles
Coma final Permesa Prohibida: {"a":1,} és error
Comentaris // i /* */ No existeixen
undefined Vàlid No existeix
Funcions Vàlides No existeixen
Dates Objecte Date No existeixen: es desen com a text
NaN, Infinity Vàlids No existeixen
Claus calculades { [c]: v } No
// JSON vàlid
'{"id": 6, "titol": "Pressupost de la fusteria", "revisor": null}'

// JSON no vàlid (encara que seria JavaScript perfectament vàlid)
"{id: 6}"                        // ✗ clau sense cometes
"{'id': 6}"                      // ✗ cometes simples
'{"id": 6,}'                     // ✗ coma final
'{"id": 6, "estat": undefined}'  // ✗ undefined no existeix a JSON

Tingues present a més que dataLimit és un string al model de Nómada Tasques. Aquella decisió de disseny del Mòdul 1, que aleshores podia semblar arbitrària, cobra aquí tot el sentit: com que JSON no té dates natives, desar '2026-09-05' significa que la dada sobreviu intacta al viatge d'anada i tornada, sense conversions ni sorpreses.

  1. JSON.stringify: convertir a text

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 text = JSON.stringify(tasca);
console.log(typeof text);   // 'string'
console.log(text);
// {"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"}

Funciona amb qualsevol valor, no només amb objectes:

console.log(JSON.stringify(42));            // '42'
console.log(JSON.stringify('hola'));        // '"hola"'   ← compte: amb cometes a dins
console.log(JSON.stringify(true));          // 'true'
console.log(JSON.stringify(null));          // 'null'
console.log(JSON.stringify([1, 2, 3]));     // '[1,2,3]'
console.log(JSON.stringify(undefined));     // undefined  ← no és un string!

Aquesta última línia sorprèn: JSON.stringify(undefined) no retorna 'undefined', retorna el valor undefined. És un detall que pot petar un localStorage.setItem al Mòdul 7.

  1. Donar format amb el tercer paràmetre

El tercer paràmetre afegeix salts de línia i sagnia. És el que vols quan el JSON el llegirà una persona:

console.log(JSON.stringify(tasca, null, 2));
{
  "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"
}

El null del mig és el segon paràmetre (el replacer, apartat següent), que aquí no fem servir. El 2 és el nombre d'espais de sagnia; també admet un string, com '\t' per a tabuladors.

Truc de depuració molt útil. console.log(objecte) de vegades retalla objectes grans o mostra [Object] als nivells profunds. console.log(JSON.stringify(objecte, null, 2)) el mostra sencer i llegible. El faràs servir constantment al Mòdul 8.

Per desar o enviar, en canvi, no formatis: la sagnia multiplica la mida del text sense aportar res a una màquina.

  1. Filtrar amb el replacer i amb l'array de claus

El segon paràmetre decideix què es serialitza. Admet dues formes.

Un array de claus, la més senzilla: només aquelles propietats passen el filtre.

console.log(JSON.stringify(tasca, ['id', 'titol', 'estat']));
// {"id":6,"titol":"Pressupost de la fusteria","estat":"pendent"}

Una funció replacer, que es crida per a cada parell clau-valor i decideix què retornar. Si retorna undefined, la propietat s'omet.

const resultatPublic = JSON.stringify(tasca, (clau, valor) => {
  if (clau === 'revisor') return undefined;          // s'omet
  if (clau === 'titol') return valor.toUpperCase();  // es transforma
  return valor;                                      // la resta, tal qual
}, 2);

console.log(resultatPublic.includes('revisor'));   // false
console.log(resultatPublic.includes('PRESSUPOST DE LA FUSTERIA'));   // true

Dos detalls del replacer: es crida també per a l'arrel, amb la clau '' i l'objecte sencer com a valor; i recorda que en un array, les claus són els índexs com a string. Si t'oblides del return valor final, tot es converteix en undefined i el resultat és buit.

Aplicació real: exportar el backlog sense els camps interns.

const CAMPS_PUBLICS = ['id', 'titol', 'responsable', 'prioritat', 'estat', 'horesEstimades', 'dataLimit'];
const exportable = JSON.stringify([tasca], CAMPS_PUBLICS, 2);
console.log(exportable.includes('revisor'));      // false
console.log(exportable.includes('etiquetes'));    // false

  1. toJSON: que un objecte decideixi com es serialitza

Si un objecte té un mètode anomenat toJSON, JSON.stringify el crida i serialitza el que retorni en lloc de l'objecte. És la manera de controlar la representació des de dins:

const tauler = {
  nom: 'Taller Nómada',
  avui: '2026-09-20',
  tasques: [tasca],
  cacheInterna: { calculatEl: 1758300000000 },     // dada interna que no volem exportar

  toJSON() {
    return {
      nom: this.nom,
      exportatEl: this.avui,
      totalTasques: this.tasques.length,
      tasques: this.tasques
    };
  }
};

console.log(JSON.stringify(tauler, null, 2).includes('cacheInterna'));   // false
console.log(JSON.parse(JSON.stringify(tauler)).totalTasques);           // 1

El mateix fa Date internament: té el seu propi toJSON que retorna la data en format ISO, i per això una data es serialitza com a text.

console.log(JSON.stringify({ creada: new Date('2026-09-20T10:30:00Z') }));
// {"creada":"2026-09-20T10:30:00.000Z"}

  1. Què es perd en el viatge d'anada i tornada

Aquest és l'apartat que cal memoritzar. Serialitzar i tornar a parsejar no retorna sempre el mateix que va entrar:

const original = {
  id: 7,
  titol: 'Revisar la guillotina',
  revisor: undefined,                       // undefined
  calcular: () => 42,                       // funció
  creada: new Date('2026-09-20'),           // Date
  progres: NaN,                             // NaN
  limit: Infinity,                          // Infinity
  etiquetes: ['enquadernació'],
  intern: Symbol('x')                       // Symbol
};

const tornada = JSON.parse(JSON.stringify(original));
console.log(tornada);
// {
//   id: 7,
//   titol: 'Revisar la guillotina',
//   creada: '2026-09-20T00:00:00.000Z',    ← ara és un STRING
//   progres: null,                          ← NaN s'ha convertit en null
//   limit: null,                            ← Infinity també
//   etiquetes: [ 'enquadernació' ]
// }
// revisor, calcular i intern han DESAPAREGUT

La taula completa:

Valor original Després de stringify + parse
string, number finit, boolean, null Igual ✓
Objecte, array Igual (però objectes nous) ✓
undefined en una propietat La propietat desapareix
undefined en un array Es converteix en null
Funció La propietat desapareix
Symbol La propietat desapareix
Date Es converteix en string ISO
NaN, Infinity, -Infinity Es converteixen en null
Map, Set Es converteixen en {} (es perd tot!)
Referència circular Llança TypeError

Els dos últims mereixen atenció especial:

console.log(JSON.stringify(new Set([1, 2, 3])));           // '{}'   ✗ buit
console.log(JSON.stringify(new Map([['a', 1]])));          // '{}'   ✗ buit

const a = { nom: 'a' };
a.joMateix = a;                                             // referència circular
// JSON.stringify(a);
// ✗ TypeError: Converting circular structure to JSON

Una referència circular és un objecte que es conté a si mateix, directament o indirectament. Passa més del que sembla: per exemple, si a cada subtasca li afegeixes un camp pare que apunta a la tasca que la conté, l'arbre deixa de ser serialitzable amb JSON.

Bona notícia per a Nómada Tasques: el model canònic està compost només de números, strings, null, arrays de strings i objectes imbricats. Sobreviu intacte al viatge. Això no és casualitat: és una conseqüència directa de les decisions de disseny del Mòdul 1.

  1. JSON.parse: tornar a convertir en objecte

const text = '{"id":6,"titol":"Pressupost de la fusteria","horesEstimades":5}';
const tasca = JSON.parse(text);

console.log(typeof tasca);          // 'object'
console.log(tasca.titol);           // 'Pressupost de la fusteria'
console.log(tasca.horesEstimades + 1);   // 6   ← és un número de debò

I amb arrays d'objectes, que és com arribarà el backlog:

const backlog = JSON.parse('[{"id":1,"horesEstimades":12},{"id":2,"horesEstimades":6}]');
console.log(Array.isArray(backlog));    // true
console.log(backlog.reduce((acc, t) => acc + t.horesEstimades, 0));   // 18

Un punt crucial: JSON.parse produeix objectes completament nous. No hi ha cap relació amb els objectes originals, encara que el contingut sigui idèntic. És justament aquesta propietat la que aprofitarem per fer còpies profundes.

  1. El reviver: transformar mentre es llegeix

El segon paràmetre de JSON.parse és una funció que es crida per a cada parell clau-valor en llegir, i permet transformar les dades en el moment. L'ús clàssic és reconstruir dates:

const desat = '{"titol":"Revisar la guillotina","creada":"2026-09-20T10:30:00.000Z"}';

const recuperat = JSON.parse(desat, (clau, valor) => {
  if (clau === 'creada' && typeof valor === 'string') return new Date(valor);
  return valor;
});

console.log(recuperat.creada instanceof Date);   // true
console.log(recuperat.creada.getFullYear());     // 2026

A Nómada Tasques el reviver serveix per normalitzar dades que arriben de fora, aplicant les regles de negoci a la porta d'entrada:

function analitzarBacklog(text) {
  return JSON.parse(text, (clau, valor) => {
    if (clau === 'etiquetes' && Array.isArray(valor)) {
      return valor.map((e) => e.trim().toLowerCase());     // R9
    }
    if (clau === 'horesEstimades' && typeof valor === 'string') {
      return Number(valor);
    }
    return valor;
  });
}

const brut = '[{"id":1,"etiquetes":[" Espai ","DISSENY"],"horesEstimades":"12"}]';
console.log(analitzarBacklog(brut));
// [ { id: 1, etiquetes: [ 'espai', 'disseny' ], horesEstimades: 12 } ]

Com el replacer, el reviver ha d'acabar amb return valor per a tot el que no transformi; si te n'oblides, el resultat serà undefined.

  1. JSON.parse sempre dins d'un try/catch

JSON.parse llança una excepció si el text no és JSON vàlid. I el text gairebé sempre ve de fora: d'un servidor, d'un fitxer, de l'emmagatzematge del navegador. Per tant, és un candidat perfecte per al try/catch de 02-05.

// ✗ Sense protecció: una dada corrupta tomba l'aplicació
// const dades = JSON.parse(textDelServidor);

// ✓ Amb protecció i un valor de reserva
function analitzarSegur(text, perDefecte = null) {
  try {
    return JSON.parse(text);
  } catch (error) {
    console.warn(`JSON no vàlid: ${error.message}`);
    return perDefecte;
  }
}

console.log(analitzarSegur('{"id":6}'));           // { id: 6 }
console.log(analitzarSegur('{id: 6}', []));        // []   (amb un avís per consola)
console.log(analitzarSegur('', []));               // []
console.log(analitzarSegur(null, []));             // null ← el valor null es converteix al text 'null', que sí que és JSON vàlid

La versió més completa distingeix l'error de sintaxi d'un contingut vàlid però inesperat, fent servir el class ErrorDeValidacio extends Error que vas conèixer a 02-05:

class ErrorDeDades extends Error {
  constructor(missatge) {
    super(missatge);
    this.name = 'ErrorDeDades';
  }
}

function carregarBacklog(text) {
  let dades;
  try {
    dades = JSON.parse(text);
  } catch (error) {
    throw new ErrorDeDades(`El fitxer no conté JSON vàlid: ${error.message}`);
  }
  if (!Array.isArray(dades)) {
    throw new ErrorDeDades('Calia un array de tasques.');
  }
  if (!dades.every((t) => typeof t.id === 'number' && typeof t.titol === 'string')) {
    throw new ErrorDeDades('Alguna tasca no té id o títol vàlids.');
  }
  return dades;
}

try {
  carregarBacklog('{"id":1}');
} catch (error) {
  console.log(`${error.name}: ${error.message}`);
  // ErrorDeDades: Calia un array de tasques.
}

Fixa't en l'ordre: primer parsejar (que pot fallar per sintaxi), després validar la forma de les dades. Parsejar correctament no garanteix que el contingut sigui el que esperes.

  1. Còpia superficial: { ...obj } i Object.assign

Tornem al mur amb què va acabar 04-07. Aquestes són les dues maneres de fer una còpia superficial, i totes dues tenen el mateix límit:

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

const copiaA = { ...tasca };
const copiaB = Object.assign({}, tasca);

console.log(copiaA.etiquetes === tasca.etiquetes);   // true  ✗ mateix array
console.log(copiaB.etiquetes === tasca.etiquetes);   // true  ✗ mateix array

copiaA.titol = 'Canviat';            // ✓ segur: string primitiu
copiaA.etiquetes.push('mobiliari');  // ✗ afecta també tasca i copiaB

console.log(tasca.titol);            // 'Redissenyar la sala polivalent'  ✓
console.log(tasca.etiquetes);        // [ 'espai', 'disseny', 'mobiliari' ]  ✗

Per a arrays, les còpies superficials equivalents són [...array], array.slice() i Array.from(array). Totes tres copien la llista, però si els elements són objectes, continuen sent els mateixos objectes:

const llista = [{ id: 1, titol: 'A' }, { id: 2, titol: 'B' }];
const copia = [...llista];

copia.push({ id: 3, titol: 'C' });
console.log(llista.length);       // 2   ✓ la llista és independent

copia[0].titol = 'MODIFICAT';
console.log(llista[0].titol);     // 'MODIFICAT'   ✗ els objectes, no

  1. Còpia profunda: structuredClone i el truc amb JSON

Una còpia profunda duplica l'objecte i tot el que conté, a qualsevol profunditat. Hi ha dues maneres pràctiques.

structuredClone(valor) és la resposta moderna: una funció global, integrada al llenguatge i als navegadors.

const tasca = {
  id: 1,
  titol: 'Redissenyar la sala polivalent',
  etiquetes: ['espai', 'disseny'],
  subtasques: [
    { id: 11, titol: 'Mesurar i aixecar el plànol', horesEstimades: 3, subtasques: [] },
    { id: 12, titol: 'Triar el mobiliari', horesEstimades: 0, subtasques: [
      { id: 121, titol: 'Demanar pressupostos', horesEstimades: 2, subtasques: [] }
    ] }
  ]
};

const clon = structuredClone(tasca);

clon.etiquetes.push('mobiliari');
clon.subtasques[1].subtasques[0].titol = 'CANVIAT';

console.log(tasca.etiquetes);                            // [ 'espai', 'disseny' ]  ✓
console.log(tasca.subtasques[1].subtasques[0].titol);    // 'Demanar pressupostos'  ✓
console.log(clon.subtasques[1].subtasques[0].titol);     // 'CANVIAT'

Les seves virtuts: conserva Date, Map, Set, RegExp i dades binàries, i gestiona referències circulars sense llançar error. El seu límit: no pot clonar funcions; si l'objecte té un mètode, llança un DataCloneError.

El truc amb JSON, que veuràs en moltíssim codi anterior a structuredClone:

const clonJSON = JSON.parse(JSON.stringify(tasca));
console.log(clonJSON.subtasques[1].subtasques[0].titol);   // 'Demanar pressupostos'

Funciona, i és simple de recordar, però arrossega totes les pèrdues de l'apartat 7: undefined, funcions i símbols desapareixen; les dates es converteixen en text; NaN i Infinity es converteixen en null; Map i Set es buiden; i una referència circular llança TypeError. A més és més lent, perquè construeix un string intermedi complet.

Quan fer servir cadascun:

  • structuredClone per defecte, sempre que el teu entorn el tingui.
  • JSON.parse(JSON.stringify(...)) només si saps que les teves dades són «JSON pur» —just el cas del backlog canònic de Nómada Tasques— o si treballes en un entorn antic.
  • Còpia superficial quan l'objecte no té nivells imbricats, o quan l'imbricat és de només lectura i ningú no el mutarà.

  1. La taula de les còpies

Tècnica Profunditat Conserva Date/Map/Set Funcions Circulars Velocitat
{ ...obj } Superficial Sí (per referència) Sí (per referència) Molt ràpida
Object.assign({}, obj) Superficial Sí (per referència) Sí (per referència) Molt ràpida
[...arr], arr.slice() Superficial Molt ràpida
structuredClone(obj) Profunda No (llança error) Ràpida
JSON.parse(JSON.stringify(obj)) Profunda No No (es perden) No (llança error) Lenta
Còpia manual nivell a nivell La que escriguis La que escriguis Compte La més ràpida si és petita

I la còpia manual, la de l'apartat 15 de 04-07, que continua sent perfectament vàlida quan l'estructura és coneguda i petita:

const copiaManual = {
  ...tasca,
  etiquetes: [...tasca.etiquetes],
  subtasques: tasca.subtasques.map((s) => ({ ...s, subtasques: [...s.subtasques] }))
};

Fixa't que només arriba fins al segon nivell de subtasques: si demà l'Iván n'afegeix un tercer, aquesta còpia torna a compartir referències. Aquest és exactament el motiu pel qual existeixen les còpies profundes automàtiques.

  1. Referències compartides: el bug del backlog

Vegem el problema amb un cas realista, el que li passaria a la Lucía a l'aplicació.

const backlog = [
  { id: 1, titol: 'Redissenyar la sala polivalent', etiquetes: ['espai', 'disseny'], horesEstimades: 12 },
  { id: 2, titol: 'Cartelleria del taller de serigrafia', etiquetes: ['serigrafia'], horesEstimades: 6 }
];

// «Faré una còpia de seguretat abans de tocar res»
const copiaSeguretat = [...backlog];           // ✗ còpia SUPERFICIAL

// L'usuari edita una tasca
backlog[0].titol = 'Redissenyar la sala gran';
backlog[0].etiquetes.push('obra');

// «Desfés»: restauro des de la còpia de seguretat
console.log(copiaSeguretat[0].titol);       // 'Redissenyar la sala gran'   ✗ la còpia TAMBÉ ha canviat
console.log(copiaSeguretat[0].etiquetes);   // [ 'espai', 'disseny', 'obra' ]  ✗

La còpia de seguretat no servia de res: [...backlog] va crear un array nou, però amb els mateixos objectes a dins.

flowchart TD
    A["backlog<br/>(array)"] --> T1["tasca 1<br/>{ titol, etiquetes }"]
    A --> T2["tasca 2"]
    B["còpia de seguretat<br/>(array NOU)"] --> T1
    B --> T2
    T1 --> E["etiquetes<br/>['espai','disseny']"]
    style T1 fill:#f9d5d5,stroke:#c66

La solució, amb còpia profunda:

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

const copiaSeguretatBona = structuredClone(backlog2);

backlog2[0].titol = 'Redissenyar la sala gran';
backlog2[0].etiquetes.push('obra');

console.log(copiaSeguretatBona[0].titol);       // 'Redissenyar la sala polivalent'  ✓
console.log(copiaSeguretatBona[0].etiquetes);   // [ 'espai', 'disseny' ]            ✓

I una regla de decisió per no equivocar-te mai:

El que faràs Còpia que necessites
Canviar només camps primitius (estat, titol) Superficial: { ...tasca, estat: 'feta' }
Canviar un array o un objecte intern Superficial + desplegar aquell nivell: { ...t, etiquetes: [...t.etiquetes] }
Desar una còpia de seguretat, un historial o enviar a un altre lloc Profunda: structuredClone
Enviar per la xarxa o desar com a text Serialitzar amb JSON.stringify

  1. Object.freeze i la seva superficialitat

Si vols impedir de debò que un objecte es modifiqui, Object.freeze el congela:

'use strict';

const PESOS = Object.freeze({ alta: 3, mitjana: 2, baixa: 1 });

console.log(Object.isFrozen(PESOS));   // true

// PESOS.alta = 99;
// ✗ TypeError: Cannot assign to read only property 'alta' of object (en mode estricte)
// Sense 'use strict', l'assignació falla EN SILENCI: no canvia res i no avisa.

Congelar impedeix tres coses: modificar propietats existents, afegir-ne de noves i esborrar-les. És l'eina correcta per a les constants de configuració del projecte —PESOS, MARQUES, ESTATS_VALIDS—, perquè const només protegeix la variable, no el contingut (04-01).

Però Object.freeze és superficial, exactament igual que el spread:

const plantilla = Object.freeze({
  prioritat: 'mitjana',
  etiquetes: ['serigrafia']
});

// plantilla.prioritat = 'alta';       // ✗ TypeError: ben protegit
plantilla.etiquetes.push('urgent');    // ✓ permès!
console.log(plantilla.etiquetes);      // [ 'serigrafia', 'urgent' ]   ✗

Per congelar en profunditat cal recórrer l'estructura, i aquí torna la recursivitat de 03-07:

function congelarProfund(objecte) {
  for (const valor of Object.values(objecte)) {
    if (valor !== null && typeof valor === 'object' && !Object.isFrozen(valor)) {
      congelarProfund(valor);
    }
  }
  return Object.freeze(objecte);
}

const plantillaSegura = congelarProfund({ prioritat: 'mitjana', etiquetes: ['serigrafia'] });

try {
  plantillaSegura.etiquetes.push('urgent');
} catch (error) {
  console.log(error.name);      // 'TypeError'
}
console.log(plantillaSegura.etiquetes);   // [ 'serigrafia' ]   ✓

La comprovació !Object.isFrozen(valor) no és un adorn: evita el bucle infinit si hi ha referències circulars. Un consell d'ús: congela les constants i les dades de configuració, no l'estat que l'aplicació modifica constantment; per a això, la disciplina d'actualitzacions immutables de 04-07 és millor eina que la prohibició.

  1. Exemple integrat: desar i recuperar el backlog

Ajuntem-ho tot en la peça que Nómada Tasques necessitarà al Mòdul 7: convertir el backlog en text per desar-lo, i recuperar-lo amb garanties.

'use strict';

const VERSIO_FORMAT = 1;

/** Serialitza el backlog amb metadades, a punt per desar o enviar. */
function exportarBacklog(tasques, avui) {
  const doc = {
    versio: VERSIO_FORMAT,
    exportatEl: avui,
    totals: {
      tasques: tasques.length,
      hores: tasques.reduce((acc, t) => acc + t.horesEstimades, 0)
    },
    tasques
  };
  return JSON.stringify(doc, null, 2);
}

/** Recupera el backlog validant format, versió i contingut. */
function importarBacklog(text) {
  let doc;
  try {
    doc = JSON.parse(text);
  } catch (error) {
    throw new ErrorDeDades(`JSON no vàlid: ${error.message}`);
  }

  if (doc?.versio !== VERSIO_FORMAT) {
    throw new ErrorDeDades(`Versió de format no admesa: ${doc?.versio}`);
  }
  if (!Array.isArray(doc.tasques)) {
    throw new ErrorDeDades('El document no conté un array de tasques.');
  }

  const invalides = doc.tasques.filter(
    (t) => typeof t.id !== 'number' || typeof t.titol !== 'string' || !Array.isArray(t.etiquetes)
  );
  if (invalides.length > 0) {
    throw new ErrorDeDades(`${invalides.length} tasca(ques) amb format incorrecte.`);
  }

  return doc.tasques;
}

// ─── Anada i tornada completa ─────────────────────────────────────
const text = exportarBacklog(backlogCanonic, '2026-09-20');
console.log(text.split('\n').slice(0, 8).join('\n'));
{
  "versio": 1,
  "exportatEl": "2026-09-20",
  "totals": {
    "tasques": 6,
    "hores": 48
  },
  "tasques": [
const recuperat = importarBacklog(text);

console.log(recuperat.length);                        // 6
console.log(recuperat[5].titol);                      // 'Pressupost de la fusteria'
console.log(recuperat.reduce((a, t) => a + t.horesEstimades, 0));   // 48

// I la comprovació decisiva: són objectes NOUS
console.log(recuperat[0] === backlogCanonic[0]);      // false
recuperat[0].etiquetes.push('obra');
console.log(backlogCanonic[0].etiquetes);             // [ 'espai', 'disseny' ]  ✓ intacte

Aquest últim bloc revela una cosa important: JSON.parse és, de fet, una còpia profunda de franc. El backlog recuperat és completament independent de l'original, i per això el push no el contamina.

Tres decisions de disseny que val la pena assenyalar:

  1. El document porta versio. Quan d'aquí a un any el model canviï, l'importador podrà reconèixer el format antic i convertir-lo, en lloc de fallar de manera incomprensible.
  2. Es valida després de parsejar. JSON vàlid no significa dades correctes.
  3. Es llança un error amb nom propi, no es retorna null. Qui cridi decideix què fer, seguint el criteri de 02-05.

Desar aquest text al navegador és literalment una línia, localStorage.setItem('nomada-tasques', text), i és el punt de partida d'Emmagatzematge Local i de Sessió. Enviar-lo a un servidor és una altra, amb fetch, a 07-02. Aquí ja tens fet l'essencial: la conversió i la seva validació.

Errors Habituals i Consells

1. Confondre l'objecte amb el seu JSON. JSON.stringify(tasca).titol és undefined: un string no té propietats.

2. Cridar JSON.parse sense try/catch. Qualsevol dada corrupta tomba el programa.

3. Parsejar dues vegades. JSON.parse(JSON.parse(text)) llança error tret de casos rars; i si la dada ja és un objecte, JSON.parse(objecte) el converteix a string primer i sol fallar amb Unexpected token o.

4. Esperar que JSON.stringify conservi les funcions. Els mètodes d'un objecte desapareixen. En recuperar-lo tindràs les dades, però no el comportament.

5. Creure que { ...obj } ho copia tot. Copia un nivell. Si hi ha arrays o objectes a dins, es comparteixen.

6. Fer servir JSON.parse(JSON.stringify(x)) amb Date, Map, Set o undefined. Es perden o es transformen en silenci. Fes servir structuredClone.

7. Confiar en Object.freeze per protegir estructures imbricades. És superficial; cal congelar en profunditat.

8. Formatar el JSON que enviaràs o desaràs. null, 2 és per llegir, no per transmetre: multiplica la mida sense cap avantatge.

Consell professional. Tracta sempre les dades externes com a sospitoses: parseja dins d'un try/catch, valida la forma just després i no confiïs en cap camp fins que l'hagis comprovat. Les tres línies que costa aquesta disciplina eviten la majoria de les fallades en producció, i al Mòdul 8 escriuràs proves precisament per als casos límit que ara estàs preveient.

Exercicis

Exercici 1 — Exportació pública. Escriu exportarPublic(tasques) que retorni un string JSON formatat amb dos espais, que contingui només els camps id, titol, responsable, estat i dataLimit de cada tasca. Resol-ho de dues maneres: amb l'array de claus de JSON.stringify i amb una funció replacer. Comprova en tots dos casos que el resultat no conté la paraula revisor.

Exercici 2 — La còpia de seguretat que sí que funciona. Escriu crearCopiaSeguretat(backlog) i restaurar(copiaSeguretat), de manera que la còpia sigui immune a qualsevol modificació posterior del backlog, incloses les etiquetes i les subtasques imbricades. Demostra amb console.log que funciona: modifica un títol, empeny una etiqueta i canvia una subtasca de segon nivell, i comprova que la còpia continua intacta. Explica per què [...backlog] no hauria servit.

Exercici 3 — Detector de pèrdues. Escriu queEsPerdAmbJSON(objecte) que retorni un array amb els noms de les propietats de primer nivell que no sobreviurien a JSON.parse(JSON.stringify(objecte)), indicant el motiu en el format 'revisor: undefined desapareix'. Ha de detectar undefined, funcions, símbols, NaN, Infinity, Date, Map i Set. Prova-ho amb un objecte que els tingui tots.

Solucions

Exercici 1

const CAMPS_PUBLICS = ['id', 'titol', 'responsable', 'estat', 'dataLimit'];

// Forma A: array de claus
function exportarPublicA(tasques) {
  return JSON.stringify(tasques, CAMPS_PUBLICS, 2);
}

// Forma B: funció replacer
function exportarPublicB(tasques) {
  return JSON.stringify(tasques, (clau, valor) => {
    if (clau === '' || !Number.isNaN(Number(clau))) return valor;   // arrel i índexs de l'array
    return CAMPS_PUBLICS.includes(clau) ? valor : undefined;
  }, 2);
}

console.log(exportarPublicA(backlogCanonic).includes('revisor'));    // false
console.log(exportarPublicB(backlogCanonic).includes('revisor'));    // false
console.log(exportarPublicA(backlogCanonic).includes('etiquetes'));  // false
console.log(JSON.parse(exportarPublicA(backlogCanonic))[5].titol);
// 'Pressupost de la fusteria'

La versió amb array de claus és molt més simple i és la que hauries de fer servir. La versió amb replacer té un parany que mereix comentari: en serialitzar un array, el replacer es crida amb la clau '' per a l'arrel i amb '0', '1', '2'… per a cada element. Si no deixes passar aquests casos, la funció retorna undefined per a tots i el resultat és un JSON buit. És l'error més freqüent amb replacers, i per això la primera línia del cos comprova explícitament l'arrel i els índexs numèrics.

Exercici 2

function crearCopiaSeguretat(backlog) {
  return { creadaEl: new Date().toISOString().slice(0, 10), tasques: structuredClone(backlog) };
}

function restaurar(copiaSeguretat) {
  return structuredClone(copiaSeguretat.tasques);
}

const backlog = [
  {
    id: 1,
    titol: 'Redissenyar la sala polivalent',
    etiquetes: ['espai', 'disseny'],
    subtasques: [
      { id: 12, titol: 'Triar el mobiliari', subtasques: [
        { id: 121, titol: 'Demanar pressupostos', subtasques: [] }
      ] }
    ]
  }
];

const copiaSeguretat = crearCopiaSeguretat(backlog);

// Tres modificacions a profunditats diferents
backlog[0].titol = 'Redissenyar la sala gran';
backlog[0].etiquetes.push('obra');
backlog[0].subtasques[0].subtasques[0].titol = 'Demanar tres pressupostos';

console.log(copiaSeguretat.tasques[0].titol);
// 'Redissenyar la sala polivalent'                   ✓ nivell 1 intacte
console.log(copiaSeguretat.tasques[0].etiquetes);
// [ 'espai', 'disseny' ]                             ✓ array intern intacte
console.log(copiaSeguretat.tasques[0].subtasques[0].subtasques[0].titol);
// 'Demanar pressupostos'                             ✓ nivell 3 intacte

const restaurat = restaurar(copiaSeguretat);
console.log(restaurat[0].titol);                          // 'Redissenyar la sala polivalent'
console.log(restaurat[0] === copiaSeguretat.tasques[0]);  // false  ← també independent

[...backlog] no hauria servit perquè només crea un array nou: els seus elements continuen sent exactament els mateixos objectes que els del backlog original, així que les tres modificacions es veurien també a la còpia. És el bug de l'apartat 14. Fixa't a més que restaurar torna a clonar: si retornés copiaSeguretat.tasques directament, l'aplicació començaria a modificar la mateixa còpia de seguretat i aquesta deixaria de servir per a un segon «desfés». En aquest cas concret, amb dades JSON pures, JSON.parse(JSON.stringify(backlog)) hauria funcionat igual de bé; structuredClone és preferible perquè no depèn que les dades siguin serialitzables.

Exercici 3

function queEsPerdAmbJSON(objecte) {
  const problemes = [];

  for (const [clau, valor] of Object.entries(objecte)) {
    if (valor === undefined) {
      problemes.push(`${clau}: undefined desapareix`);
    } else if (typeof valor === 'function') {
      problemes.push(`${clau}: les funcions desapareixen`);
    } else if (typeof valor === 'symbol') {
      problemes.push(`${clau}: els símbols desapareixen`);
    } else if (typeof valor === 'number' && !Number.isFinite(valor)) {
      problemes.push(`${clau}: ${valor} es converteix en null`);
    } else if (valor instanceof Date) {
      problemes.push(`${clau}: Date es converteix en string`);
    } else if (valor instanceof Map || valor instanceof Set) {
      problemes.push(`${clau}: ${valor.constructor.name} es converteix en {}`);
    }
  }

  return problemes;
}

const sospitos = {
  id: 7,
  titol: 'Revisar la guillotina',
  revisor: undefined,
  calcular: () => 42,
  intern: Symbol('x'),
  progres: NaN,
  limit: Infinity,
  creada: new Date('2026-09-20'),
  vistes: new Set(['Marta']),
  cache: new Map()
};

console.log(queEsPerdAmbJSON(sospitos));
// [ 'revisor: undefined desapareix',
//   'calcular: les funcions desapareixen',
//   'intern: els símbols desapareixen',
//   'progres: NaN es converteix en null',
//   'limit: Infinity es converteix en null',
//   'creada: Date es converteix en string',
//   'vistes: Set es converteix en {}',
//   'cache: Map es converteix en {}' ]

Tres apunts tècnics. Number.isFinite(valor) captura d'un sol cop NaN, Infinity i -Infinity, i és preferible a comprovar-los per separat; recorda de 01-07 que NaN !== NaN, així que comparar-lo directament no funcionaria. instanceof comprova de quin «motlle» procedeix un objecte, i és exactament el mecanisme que estudiaràs a la lliçó següent en veure els prototips. I una limitació honesta d'aquesta funció: només revisa el primer nivell; una versió completa hauria de recórrer l'estructura recursivament, amb la tècnica de 03-07 i la mateixa precaució amb les referències circulars que vas aplicar a congelarProfund.

Conclusió

Amb aquesta lliçó tanques el Mòdul 4. Saps que JSON és text, no un objecte, i coneixes la taula de diferències que provoca la majoria dels errors: cometes dobles obligatòries a claus i strings, sense coma final, sense comentaris, sense undefined, sense funcions i sense dates natives. Domines JSON.stringify amb els seus tres paràmetres —el replacer com a funció o com a array de claus, i el null, 2 que el fa llegible—, saps que un objecte pot decidir la seva pròpia representació amb toJSON, i tens memoritzat què es perd en el viatge: undefined, funcions i símbols desapareixen; Date es converteix en string; NaN i Infinity en null; Map i Set es buiden; i una referència circular llança TypeError. També saps que JSON.parse admet un reviver per normalitzar al vol i que mai no s'ha de cridar sense try/catch, perquè el text sempre ve de fora.

I has resolt el mur que va deixar la lliçó anterior. Una còpia superficial{ ...obj }, Object.assign, [...arr], slice()— duplica un sol nivell i comparteix tot l'imbricat, cosa que converteix una «còpia de seguretat» en una il·lusió perillosa. Una còpia profunda ho duplica tot: structuredClone com a opció per defecte —conserva Date, Map, Set i les referències circulars, però no les funcions— i el vell truc JSON.parse(JSON.stringify(...)) quan les dades són JSON pur, com és el cas del backlog canònic. Tens la taula comparativa, la regla de decisió sobre quina còpia necessita cada operació, i Object.freeze per blindar les constants del projecte, amb l'advertiment que també és superficial i que congelar en profunditat exigeix recursivitat. Finalment has escrit exportarBacklog i importarBacklog, amb versió de format, validació després del parseig i errors amb nom propi: la base del que al Mòdul 7 es convertirà en persistència real.

Mira ara el mòdul sencer. Vas començar amb sis arrays paral·lels i descriureTasca demanant vuit arguments. Acabes amb un model de dades de debò: cada tasca és un objecte amb nou camps, el backlog és un array d'objectes, i sobre ell tens un tauler amb mètodes i this, el catàleg complet d'operacions d'array, les sis formes d'iterar amb criteri per triar, l'arsenal de find/filter/sort/reduce que produeix l'informe setmanal de la Marta, la desestructuració que fa llegibles signatures i bucles, el spread que permet actualitzar sense destruir, i la serialització que treu les dades de la memòria. Els mateixos números de sempre —48 h, 45 obertes, l'Iván amb 25, la fusteria vençuda, esforç 124— però ara calculats amb dues línies en lloc de vint.

Queda, però, una repetició que no has sabut evitar. Cada vegada que vols que una tasca sàpiga descriure's, validar-se o marcar-se com a feta, has d'escriure aquests mètodes dins de cada objecte, o mantenir-los com a funcions soltes a les quals passes la tasca. Amb sis tasques és incòmode; amb sis-centes, cadascuna arrossegaria la seva pròpia còpia dels mateixos mètodes, ocupant memòria per fer exactament el mateix. I ja has vist pistes que el llenguatge té una resposta: instanceof, que pregunta de quin «motlle» procedeix un objecte; new Tasca(...), aquella tercera forma d'invocació que vam deixar anotada a 04-02; el class ErrorDeValidacio extends Error que vas fer servir com a recepta sense entendre'l del tot. Totes apunten al mateix: cal un motlle compartit del qual totes les tasques heretin el seu comportament, en lloc de repetir-lo objecte a objecte. Aquest mecanisme és el cor de JavaScript i l'arrencada del Mòdul 5: Objectes i Funcions Avançades, que comença amb Prototips i Herència i continua amb les classes, l'encapsulació, els mòduls i —l'altre gran salt del curs— l'asincronia.

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