La lliçó anterior va acabar amb una promesa incomplerta. Vas escriure canviarEstat perquè les transicions respectessin la regla R6, i tanmateix qualsevol línia del programa continua podent escriure tasca.estat = 'feta' i deixar l'objecte en un estat impossible. El mètode existeix, però no és obligatori: és una recomanació educada. Aquesta lliçó va de convertir aquella recomanació en una barrera real. Aprendràs a exposar propietats calculades amb get, a interceptar les escriptures amb set, a amagar l'estat de debò amb camps privats #camp —una característica del llenguatge, no una convenció—, a declarar propietats de només lectura amb Object.defineProperty, i a dissenyar una API pública mínima per a Tauler. I com que el que és privat interfereix amb la serialització que vas aprendre a 04-08, tancaràs amb toJSON perquè exportar el backlog continuï funcionant.

Contingut

  1. Què pot trencar un consumidor sense encapsulació
  2. get: propietats calculades
  3. set: validar en l'assignació
  4. Quan un getter i quan un mètode
  5. Camps privats #camp
  6. Mètodes privats i estàtics privats
  7. L'operador #camp in objecte
  8. Les alternatives històriques: _camp, closures i WeakMap
  9. Propietats de només lectura i Object.defineProperty
  10. Dissenyar l'API pública de Tauler
  11. El que és privat i JSON.stringify: el rescat de toJSON
  12. Errors Habituals i Consells
  13. Exercicis
  14. Conclusió

  1. Què pot trencar un consumidor sense encapsulació

Comencem mesurant el mal. Aquesta és la Tasca del 05-02, amb tota la seva validació, i aquestes són quatre línies que la destrueixen sense que l'objecte protesti.

'use strict';

const tasca = new Tasca({
  id: 6, titol: 'Pressupost de la fusteria', responsable: 'Iván',
  prioritat: 'alta', horesEstimades: 5, dataLimit: '2026-09-05'
});

// ✗ 1 · Se salta la R6: de 'pendent' a 'feta' sense passar per 'en-curs'
tasca.estat = 'feta';

// ✗ 2 · Se salta la R3: hores fora del rang 1-40
tasca.horesEstimades = 500;

// ✗ 3 · Estat inexistent: ni tan sols és a Tasca.ESTATS
tasca.estat = 'arxivada';

// ✗ 4 · Se salta la R8: cadena buida en lloc de null
tasca.responsable = '';

console.log(tasca.descripcio('2026-09-20'));
// ? [6] Pressupost de la fusteria ·  · 500 h

Aquell ? de la marca i aquell buit on hauria d'anar el responsable són les conseqüències visibles. Les invisibles són pitjors: el resum del tauler ara diu que hi ha 543 hores obertes, l'informe de la Marta quadra malament, i quan algú depuri el problema d'aquí a tres setmanes no hi haurà manera de saber quina línia va fer l'assignació il·legal, perquè una assignació no deixa rastre en cap traça.

El diagnòstic tècnic és simple: estat, horesEstimades i responsable són propietats públiques normals, i a JavaScript una propietat pública és un contracte obert. L'encapsulació consisteix a reduir la superfície de contacte: deixar visible només allò que el consumidor necessita, i obligar que tot canvi passi per un mètode que sàpiga validar-lo.

flowchart LR
    subgraph Sense["Sense encapsulació"]
        C1["Codi extern"] -->|"tasca.estat = 'feta'"| E1["estat<br/>(públic)"]
        C1 -->|"tasca.canviarEstat('feta')"| M1["canviarEstat()"]
        M1 --> E1
    end
    subgraph Amb["Amb encapsulació"]
        C2["Codi extern"] -.->|"✗ inaccessible"| E2["#estat<br/>(privat)"]
        C2 -->|"tasca.canviarEstat('feta')"| M2["canviarEstat()<br/>valida R6"]
        M2 --> E2
    end

  1. get: propietats calculades

El primer pas és més suau que els camps privats i ja resol molts casos: una propietat que no es guarda, es calcula.

'use strict';

class Tasca {
  constructor(dades) {
    this.id = dades.id;
    this.titol = dades.titol;
    this.estat = dades.estat ?? 'pendent';
    this.horesEstimades = dades.horesEstimades;
    this.dataLimit = dades.dataLimit;
  }

  get oberta() {
    return this.estat !== 'feta';
  }

  get diesRestants() {
    const limit = new Date(this.dataLimit);
    const avui = new Date('2026-09-20');
    return Math.round((limit - avui) / (1000 * 60 * 60 * 24));
  }
}

const fusteria = new Tasca({ id: 6, titol: 'Pressupost de la fusteria', horesEstimades: 5, dataLimit: '2026-09-05' });

console.log(fusteria.oberta);          // true    ← sense parèntesis
console.log(fusteria.diesRestants);    // -15     ← vençuda fa 15 dies

Les dues característiques que defineixen un getter:

  • Es llegeix com una propietat, sense parèntesis. fusteria.oberta, no fusteria.oberta().
  • S'executa cada vegada que es llegeix, així que el valor sempre està al dia. Si canvies estat, oberta canvia sola: no hi ha cap dada duplicada per mantenir sincronitzada.

Aquest segon punt és l'argument fort. L'alternativa —guardar this.oberta = true al constructor— crea dues fonts de veritat que tard o d'hora es dessincronitzen.

// ✗ Dada derivada guardada: cal recordar-se d'actualitzar-la SEMPRE
this.oberta = dades.estat !== 'feta';
tasca.estat = 'feta';
console.log(tasca.oberta);         // true  ← mentida

Un getter també funciona en objectes literals, no només en classes, amb la mateixa sintaxi:

const equip = {
  membres: ['Marta', 'Iván', 'Lucía'],
  get mida() { return this.membres.length; }
};
console.log(equip.mida);           // 3

I al Tauler els getters brillen especialment, perquè gairebé tot el que es consulta és derivat:

class Tauler {
  constructor(nom, tasques = []) {
    this.nom = nom;
    this.tasques = [...tasques];
  }

  get total()          { return this.tasques.length; }
  get obertes()        { return this.tasques.filter((t) => t.oberta); }
  get horesTotals()    { return this.tasques.reduce((s, t) => s + t.horesEstimades, 0); }
  get horesObertes()   { return this.obertes.reduce((s, t) => s + t.horesEstimades, 0); }
  get esforc()         { return this.tasques.reduce((s, t) => s + t.esforc(), 0); }
}

console.log(tauler.total);           // 6
console.log(tauler.horesTotals);     // 48
console.log(tauler.horesObertes);    // 45
console.log(tauler.esforc);          // 124

Fixa't en horesObertes: fa servir el getter obertes, que al seu torn recorre tasques. Els getters es poden encadenar entre si amb tota naturalitat, i el resultat es llegeix com una descripció de les dades, no com una seqüència de crides.

  1. set: validar en l'assignació

Un setter és l'altra meitat: una funció que s'executa quan algú assigna a la propietat. Rep exactament un paràmetre, el valor assignat.

'use strict';

class Tasca {
  constructor(dades) {
    this.id = dades.id;
    this._hores = 0;
    this.horesEstimades = dades.horesEstimades;   // ← passa pel setter, es valida
  }

  get horesEstimades() {
    return this._hores;
  }

  set horesEstimades(valor) {
    if (typeof valor !== 'number' || Number.isNaN(valor)) {
      throw new ErrorDeValidacio('Les hores han de ser un nombre.', 'horesEstimades', valor);
    }
    if (valor <= 0 || valor > 40) {                                              // R3
      throw new ErrorDeValidacio('Les hores han de ser entre 1 i 40.', 'horesEstimades', valor);
    }
    this._hores = valor;
  }
}

const t = new Tasca({ id: 6, horesEstimades: 5 });
console.log(t.horesEstimades);      // 5

t.horesEstimades = 8;               // ✓ vàlid
console.log(t.horesEstimades);      // 8

try {
  t.horesEstimades = 500;           // ✗ ara SÍ que salta
} catch (error) {
  console.error(error.message);     // 'Les hores han de ser entre 1 i 40.'
}
console.log(t.horesEstimades);      // 8   ← l'objecte ha quedat intacte

Ja no hi ha manera de ficar 500 hores en una tasca, ni tan sols per assignació directa. Quatre detalls que cal tenir clars:

  • El getter i el setter comparteixen nom, i aquell nom no pot ser el del camp on es guarda la dada. Si escrivissis set horesEstimades(v) { this.horesEstimades = v; } provocaries una recursió infinita: l'assignació torna a cridar el setter. Per això el magatzem intern es diu diferent (_hores aquí, i #hores així que arribem a l'apartat 5).
  • Un setter sense getter fa que llegir la propietat retorni undefined. Gairebé sempre es declaren tots dos.
  • Un getter sense setter converteix la propietat en només lectura: en mode estricte, assignar-li alguna cosa llança TypeError. És un ús perfectament legítim.
  • Validar al constructor no n'hi ha prou. Fixa't que el constructor de dalt assigna this.horesEstimades = dades.horesEstimades en lloc de tocar _hores directament: així la validació del setter s'aplica també en la creació, i la regla viu escrita una sola vegada.
class NomesLectura {
  #valor = 42;
  get valor() { return this.#valor; }
}

const nl = new NomesLectura();
// nl.valor = 7;      // ✗ TypeError: Cannot set property valor… which has only a getter

  1. Quan un getter i quan un mètode

Getters i mètodes fan coses semblants, i triar malament produeix APIs incòmodes. La regla pràctica:

Fes servir un getter quan… Fes servir un mètode quan…
Retorna una dada conceptualment pròpia de l'objecte Fa una acció o un càlcul amb nom de verb
És barat (recórrer un array curt, una resta) És car (recórrer milers d'elements, ordenar, clonar)
No rep arguments Rep arguments
No té efectes secundaris ni llança errors en condicions normals Pot modificar l'objecte o fallar
Llegir-lo dues vegades seguides dona el mateix El resultat pot canviar entre crides

Aplicat al projecte:

class Tasca {
  get oberta()         { … }          // ✓ dada pròpia, barata, sense arguments
  get diesRestants()   { … }          // ✓ derivat de dataLimit

  estaVencuda(avui)    { … }          // ✓ mètode: rep un argument
  canviarEstat(nou)    { … }          // ✓ mètode: és una ACCIÓ, i pot llançar
  esforc()             { … }          // ✓ mètode: podria ser getter; es manté per coherència
}

El criteri del cost mereix un avís, perquè és el que més sorprèn. Qui llegeix tauler.horesObertes dona per fet que és una lectura barata, i no s'ho pensa dues vegades abans de posar-ho dins d'un bucle:

// Si horesObertes fos costós, això seria un desastre invisible
for (const tasca of tauler.tasques) {
  console.log(tasca.horesEstimades / tauler.horesObertes);   // ← recalcula a cada volta
}

Amb sis tasques és igual. Amb cinc mil, aquell bucle passa a ser quadràtic i ningú no sospitaria d'una simple lectura de propietat. La regla és honesta: si el càlcul és car, dona-li forma de mètode (calcularHoresObertes()), perquè els parèntesis avisen que allà hi passa alguna cosa. I si necessites un getter car, guarda el resultat a la memòria cau internament i invalida'l quan l'estat canviï —una tècnica que és la memoïtzació de 03-06 aplicada a objectes, i que s'estudia amb calma al Mòdul 9.

  1. Camps privats #camp

Els getters i setters controlen l'accés, però a l'apartat 3 el magatzem real continuava sent _hores: una propietat pública amb un nom que demana que no la toquis. Des d'ES2022 el llenguatge ofereix privacitat de debò.

'use strict';

class Tasca {
  #estat = 'pendent';             // camp privat, amb valor inicial (R5)

  constructor(dades) {
    this.id = dades.id;
    this.titol = dades.titol;
    if (dades.estat !== undefined) this.#validarIAssignarEstat(dades.estat, true);
  }

  get estat() {
    return this.#estat;           // lectura pública, escriptura impossible
  }

  canviarEstat(nou) {
    this.#validarIAssignarEstat(nou, false);
    return this;
  }

  #validarIAssignarEstat(nou, esInicial) { /* apartat 6 */ }
}

const t = new Tasca({ id: 6, titol: 'Pressupost de la fusteria' });

console.log(t.estat);             // 'pendent'   ← es llegeix pel getter
// t.#estat;                      // ✗ SyntaxError: Private field '#estat' must be declared in an enclosing class
// t.estat = 'feta';              // ✗ TypeError: només té getter

Els camps privats tenen unes propietats molt concretes que convé enumerar:

  • El coixinet forma part del nom. #estat i estat són propietats diferents que poden coexistir, que és justament el que fem: #estat guarda, estat (getter) exposa.
  • S'han de declarar al cos de la classe. No en pots crear un al vol amb this.#nou = 1 si no està declarat: és un SyntaxError, detectat abans d'executar.
  • Només són accessibles des de dins de la classe que els declara. Ni tan sols des d'una subclasse: si TascaRecurrent extends Tasca necessita l'estat, ha de fer servir el getter públic.
  • L'error és sintàctic, no d'execució. Accedir-hi des de fora ni tan sols compila, cosa que significa que la fallada apareix immediatament i no en producció.
  • No apareixen a Object.keys, for...in, JSON.stringify, Object.entries ni al spread {...obj}. Això últim té conseqüències que resoldrem a l'apartat 11.

Aquesta és la privacitat real, i és el que faltava per complir la promesa del 05-02:

const tasca = new Tasca({ id: 6, titol: 'Pressupost de la fusteria' });

tasca.estat = 'feta';                       // en mode estricte → TypeError
console.log(tasca.estat);                   // 'pendent'  ← intacta

tasca.canviarEstat('en-curs');              // ✓ únic camí
console.log(tasca.estat);                   // 'en-curs'

  1. Mètodes privats i estàtics privats

El coixinet també funciona en mètodes, camps estàtics i mètodes estàtics. Serveix per al mateix propòsit: separar el mecanisme intern de la interfície.

Aquesta és la versió completa de la Tasca encapsulada del projecte, l'exemple central de la lliçó:

'use strict';

class Tasca {
  // ── Configuració privada de la classe ───────────────────────────
  static #TRANSICIONS = {                        // R6
    pendent: ['en-curs'],
    'en-curs': ['pendent', 'feta'],
    feta: []
  };
  static #MARQUES = { pendent: '○', 'en-curs': '▸', feta: '✓' };
  static #PESOS = { alta: 3, mitjana: 2, baixa: 1 };
  static #ultimId = 6;                           // R1: el backlog arriba fins al 6

  // ── Estat privat de la instància ────────────────────────────────
  #estat = 'pendent';                            // R5
  #hores;
  #etiquetes = [];

  constructor(dades) {
    this.id = dades.id ?? Tasca.seguentId();
    this.titol = Tasca.#validarTitol(dades.titol);
    this.responsable = dades.responsable || null;                 // R8
    this.prioritat = dades.prioritat ?? 'mitjana';
    this.dataLimit = dades.dataLimit;
    this.revisor = dades.revisor ?? null;

    this.horesEstimades = dades.horesEstimades;                   // passa pel setter (R3)
    this.etiquetes = dades.etiquetes ?? [];                       // passa pel setter (R9)
    if (dades.estat !== undefined) this.#assignarEstatInicial(dades.estat);
  }

  // ── Estàtics ────────────────────────────────────────────────────
  static seguentId() {
    Tasca.#ultimId += 1;
    return Tasca.#ultimId;
  }

  static #validarTitol(titol) {                                   // mètode estàtic PRIVAT
    if (typeof titol !== 'string' || titol.trim() === '') {
      throw new ErrorDeValidacio('El títol no pot estar buit.', 'titol', titol);   // R2
    }
    return titol.trim();
  }

  // ── Getters ─────────────────────────────────────────────────────
  get estat()          { return this.#estat; }
  get horesEstimades() { return this.#hores; }
  get etiquetes()      { return [...this.#etiquetes]; }           // còpia defensiva
  get oberta()         { return this.#estat !== 'feta'; }
  get esforc()         { return (Tasca.#PESOS[this.prioritat] ?? 0) * this.#hores; }
  get marca()          { return Tasca.#MARQUES[this.#estat]; }

  // ── Setters ─────────────────────────────────────────────────────
  set horesEstimades(valor) {
    if (typeof valor !== 'number' || !(valor > 0 && valor <= 40)) {
      throw new ErrorDeValidacio('Les hores han de ser un nombre entre 1 i 40.', 'horesEstimades', valor);
    }
    this.#hores = valor;
  }

  set etiquetes(llista) {
    if (!Array.isArray(llista)) {
      throw new ErrorDeValidacio('Les etiquetes han de ser un array.', 'etiquetes', llista);
    }
    this.#etiquetes = [...new Set(llista.map((e) => String(e).trim().toLowerCase()))].filter(Boolean);   // R9
  }

  // ── Mètodes privats ─────────────────────────────────────────────
  #assignarEstatInicial(estat) {
    if (!Object.hasOwn(Tasca.#TRANSICIONS, estat)) {
      throw new ErrorDeValidacio(`Estat desconegut: "${estat}".`, 'estat', estat);
    }
    this.#estat = estat;
  }

  #potAnarA(nou) {
    return (Tasca.#TRANSICIONS[this.#estat] ?? []).includes(nou);
  }

  // ── Interfície pública ──────────────────────────────────────────
  canviarEstat(nou) {
    if (!Object.hasOwn(Tasca.#TRANSICIONS, nou)) {
      throw new ErrorDeValidacio(`Estat desconegut: "${nou}".`, 'estat', nou);
    }
    if (!this.#potAnarA(nou)) {
      throw new ErrorDeValidacio(
        `Transició no permesa: "${this.#estat}" → "${nou}".`, 'estat', nou);   // R6
    }
    this.#estat = nou;
    return this;
  }

  estaVencuda(avui) {
    return this.dataLimit < avui && this.oberta;                  // R10
  }

  descripcio(avui) {
    const avis = avui !== undefined && this.estaVencuda(avui) ? ' ⚠ VENÇUDA' : '';
    return `${this.marca} [${this.id}] ${this.titol} · ${this.responsable ?? 'sense assignar'} · ${this.#hores} h${avis}`;
  }
}

I la comprovació que l'objecte ja no es pot corrompre:

const fusteria = new Tasca({
  id: 6, titol: '  Pressupost de la fusteria  ', responsable: 'Iván',
  prioritat: 'alta', etiquetes: ['Fusteria', 'compres', 'FUSTERIA', '  '],
  horesEstimades: 5, dataLimit: '2026-09-05', revisor: 'Marta'
});

console.log(fusteria.titol);                    // 'Pressupost de la fusteria'  ← trim, R2
console.log(fusteria.etiquetes);                // [ 'fusteria', 'compres' ]    ← R9
console.log(fusteria.descripcio('2026-09-20')); // ○ [6] Pressupost de la fusteria · Iván · 5 h ⚠ VENÇUDA
console.log(fusteria.esforc);                   // 15

// Els quatre atacs de l'apartat 1, ara:
try { fusteria.estat = 'feta'; }             catch (e) { console.log('✓ bloquejat:', e.constructor.name); }
try { fusteria.horesEstimades = 500; }       catch (e) { console.log('✓ bloquejat:', e.message); }
try { fusteria.canviarEstat('arxivada'); }   catch (e) { console.log('✓ bloquejat:', e.message); }
try { fusteria.canviarEstat('feta'); }       catch (e) { console.log('✓ bloquejat:', e.message); }

console.log(fusteria.estat, fusteria.horesEstimades);   // 'pendent' 5   ← intacte

Val la pena assenyalar dues decisions de disseny d'aquell codi:

La còpia defensiva del getter etiquetes. Retornar [...this.#etiquetes] en lloc de l'array intern impedeix que algú faci tasca.etiquetes.push('URGENT') i se salti la R9 mutant des de fora. És exactament el perill de la còpia superficial que vas estudiar a 04-08, aplicat aquí com a defensa. El preu és que crea un array nou a cada lectura, així que es justifica només quan l'array és petit i el risc és real.

Els setters es fan servir també des del constructor. this.horesEstimades = dades.horesEstimades dins del constructor invoca el setter i aplica la R3. Si el constructor escrivís this.#hores = dades.horesEstimades, la validació existiria només per a les modificacions posteriors, i la regla acabaria duplicada. Una regla, un lloc.

  1. L'operador #camp in objecte

Com que accedir a un camp privat des de fora és un SyntaxError, no se'n pot comprovar l'existència amb un try/catch. Per a això existeix una forma especial de l'operador in, anomenada brand check («comprovació de marca»):

'use strict';

class Tasca {
  #estat = 'pendent';

  /** És aquest objecte una Tasca autèntica, creada per aquesta classe? */
  static esTasca(objecte) {
    return #estat in objecte;
  }
}

const real = new Tasca({ id: 1, titol: 'Redissenyar la sala polivalent', horesEstimades: 12, dataLimit: '2026-09-30' });
const impostor = { id: 1, titol: 'Redissenyar la sala polivalent', estat: 'pendent' };

console.log(Tasca.esTasca(real));        // true
console.log(Tasca.esTasca(impostor));    // false
console.log(Tasca.esTasca(null));        // false  (in retorna false, no llança, per a objectes)

L'expressió #estat in objecte només es pot escriure dins de la classe que declara #estat, i retorna true o false sense llançar mai. Per què no fer servir instanceof, que ja coneixes de 05-01? Perquè instanceof mira la cadena de prototips, i aquella es pot falsificar:

const falsificat = Object.create(Tasca.prototype);
console.log(falsificat instanceof Tasca);      // true   ← menteix!
console.log(Tasca.esTasca(falsificat));        // false  ← la marca no es pot falsificar
// falsificat.descripcio();                    // ✗ TypeError: no té els camps privats

Aquell objecte té els mètodes però cap dels camps privats, perquè mai no va passar pel constructor. Un instanceof el donaria per bo i el programa rebentaria més endavant, en un lloc confús. El brand check és la comprovació honesta: només els objectes que realment van passar pel constructor d'aquesta classe tenen els seus camps privats. És la tècnica adequada quan un mètode necessita garantir que pot tocar l'estat intern del seu argument.

  1. Les alternatives històriques: _camp, closures i WeakMap

Els camps # van arribar el 2022. Abans es feien servir tres tècniques, i les trobaràs en qualsevol codi amb uns quants anys, així que convé reconèixer-les.

La convenció del guió baix. Un _ davant del nom significa «això és intern, no ho toquis». No hi ha cap mecanisme al darrere: és un acord entre persones.

class TascaAntiga {
  constructor(estat) { this._estat = estat; }
  get estat() { return this._estat; }
}
const t = new TascaAntiga('pendent');
t._estat = 'arxivada';        // funciona perfectament: res no ho impedeix

Closures (el patró mòdul de 03-04). L'estat viu en variables capturades per les funcions retornades. És privacitat real, la mateixa de crearMagatzemTasques.

function crearTasca(dadesInicials) {
  let estat = 'pendent';                        // ← inaccessible des de fora, de debò
  const transicions = { pendent: ['en-curs'], 'en-curs': ['pendent', 'feta'], feta: [] };

  return {
    id: dadesInicials.id,
    titol: dadesInicials.titol,
    get estat() { return estat; },
    canviarEstat(nou) {
      if (!transicions[estat].includes(nou)) throw new ErrorDeValidacio('Transició no permesa.', 'estat', nou);
      estat = nou;
      return this;
    }
  };
}

Funciona, però té el defecte que va motivar tot el mòdul: els mètodes es creen de nou a cada crida, perquè viuen a l'objecte retornat i no en un prototip. És el problema de l'apartat 1 de 05-01.

WeakMap amb la instància com a clau. El truc que feien servir les llibreries serioses abans d'ES2022: una taula externa que associa cada instància amb les seves dades privades.

const privats = new WeakMap();

class TascaWeak {
  constructor(dades) {
    privats.set(this, { estat: 'pendent' });
    this.id = dades.id;
  }
  get estat() { return privats.get(this).estat; }
  canviarEstat(nou) { privats.get(this).estat = nou; return this; }
}

És privacitat real i compatible amb prototips, però verbosa. El detall de la Weak: manté referències febles, de manera que quan la instància deixa de fer-se servir el recol·lector d'escombraries se'n pot endur també la seva entrada. Un Map normal aquí provocaria una fuita de memòria, tema del Mòdul 9.

La comparativa completa:

Tècnica Privacitat real? Mètodes al prototip Verbositat Quan la veuràs
_camp (convenció) No Mínima Codi anterior al 2022, i en equips que prefereixen no bloquejar les proves
Closure (patró mòdul) No Mitjana Funcions fàbrica, mòduls de configuració
WeakMap extern Alta Llibreries anteriors a ES2022
#camp Mínima L'opció per defecte avui

Amb una excepció honesta sobre #: com que és privacitat real, les teves proves tampoc no poden mirar-hi dins (Mòdul 8). Això obliga a provar l'objecte a través de la seva interfície pública, que és el correcte, però exigeix dissenyar bé aquella interfície. Alguns equips continuen preferint _ per aquesta raó; és una decisió d'equip defensable, no un error.

  1. Propietats de només lectura i Object.defineProperty

Queda un forat obert al model: l'id. És una propietat pública normal, i la R1 diu que l'assigna l'aplicació i ningú no el canvia.

fusteria.id = 999;      // ✗ res no ho impedeix encara

Un getter sense setter sobre un camp privat ja ho resoldria, però hi ha una eina més precisa i de propòsit general: Object.defineProperty, que permet crear una propietat especificant-ne els atributs en lloc de només el valor.

'use strict';

class Tasca {
  constructor(dades) {
    Object.defineProperty(this, 'id', {
      value: dades.id,
      writable: false,       // no es pot reassignar
      enumerable: true,      // sí que apareix a Object.keys i JSON.stringify
      configurable: false    // no es pot esborrar ni redefinir
    });
    this.titol = dades.titol;
  }
}

const t = new Tasca({ id: 6, titol: 'Pressupost de la fusteria' });

console.log(t.id);                      // 6
// t.id = 999;                          // ✗ TypeError: Cannot assign to read only property 'id'
// delete t.id;                         // ✗ TypeError: Cannot delete property 'id'
console.log(Object.keys(t));            // [ 'id', 'titol' ]
console.log(JSON.stringify(t));         // {"id":6,"titol":"Pressupost de la fusteria"}

Els tres atributs, amb el seu efecte pràctic:

Atribut Si és true Si és false Per defecte amb defineProperty
writable Es pot reassignar amb = Assignar llança TypeError en mode estricte (i s'ignora sense ell) false
enumerable Apareix a Object.keys, for...in, spread, JSON.stringify Existeix però es manté invisible a aquestes operacions false
configurable Es pot esborrar amb delete o redefinir delete i redefinir llancen TypeError false

Parany clàssic: una propietat creada amb assignació normal (this.id = 6) té els tres atributs a true; una creada amb Object.defineProperty els té a false si no els indiques. Si defineixes una propietat i després «desapareix» de JSON.stringify, és que et vas oblidar d'enumerable: true.

Pots inspeccionar els atributs de qualsevol propietat:

console.log(Object.getOwnPropertyDescriptor(t, 'id'));
// { value: 6, writable: false, enumerable: true, configurable: false }

console.log(Object.getOwnPropertyDescriptor(t, 'titol'));
// { value: '…', writable: true, enumerable: true, configurable: true }

I això explica per fi dues coses que van quedar soltes en lliçons anteriors. La primera, per què els mètodes d'Object.prototype no sortien a for...in a 05-01: són no enumerables. La segona, com fer que un mètode sigui invisible a l'enumeració sense fer servir class:

Object.defineProperty(Tasca.prototype, 'ocult', { value() { return 1; }, enumerable: false });

Una comparació amb Object.freeze, que vas conèixer a 04-08:

Eina Abast Ús típic
writable: false en una propietat Una propietat L'id d'una tasca (R1)
Object.freeze(obj) Totes les propietats de l'objecte, superficialment Les constants PESOS, MARQUES
#camp privat Invisible des de fora, escrivible des de dins L'#estat, que sí que canvia per mètodes

Les tres es combinen segons el que vulguis: invisible (privat), immutable (writable: false) o totes dues coses.

  1. Dissenyar l'API pública de Tauler

Amb totes les peces, l'exercici de disseny més valuós: decidir què es veu i què no a la peça central del projecte. La pregunta que guia la decisió és sempre la mateixa: què necessita de debò qui fa servir aquest objecte?

A la classe Tauler del 05-02, this.tasques era públic. Això significa que qualsevol pot fer tauler.tasques.push(cosaQualsevol), saltant-se la validació d'afegir, la R1 d'identificadors únics i la R7 de càrrega màxima. La llista de tasques és precisament l'invariant que el tauler ha de protegir.

'use strict';

class Tauler {
  #tasques = [];
  #nom;

  constructor(nom, tasques = []) {
    this.#nom = nom;
    for (const tasca of tasques) this.afegir(tasca);      // tota entrada passa per la porta oficial
  }

  // ── Lectures ────────────────────────────────────────────────────
  get nom()           { return this.#nom; }
  get total()         { return this.#tasques.length; }
  get tasques()       { return [...this.#tasques]; }              // còpia: mutar-la no afecta el tauler
  get obertes()       { return this.#tasques.filter((t) => t.oberta); }
  get horesTotals()   { return this.#tasques.reduce((s, t) => s + t.horesEstimades, 0); }
  get horesObertes()  { return this.obertes.reduce((s, t) => s + t.horesEstimades, 0); }
  get esforc()        { return this.#tasques.reduce((s, t) => s + t.esforc, 0); }

  cercarPerId(id)     { return this.#tasques.find((t) => t.id === id) ?? null; }
  filtrar(predicat)   { return this.#tasques.filter(predicat); }

  vencudes(avui)      { return this.obertes.filter((t) => t.estaVencuda(avui)); }

  horesPerResponsable() {
    return this.obertes.reduce((acc, t) => {
      const clau = t.responsable ?? 'sense assignar';
      acc[clau] = (acc[clau] ?? 0) + t.horesEstimades;
      return acc;
    }, {});
  }

  resum(avui) {
    return {
      total: this.total, obertes: this.obertes.length,
      horesTotals: this.horesTotals, horesObertes: this.horesObertes,
      vencudes: this.vencudes(avui).length, esforc: this.esforc
    };
  }

  // ── Escriptures (les úniques portes) ────────────────────────────
  afegir(tasca) {
    if (!(tasca instanceof Tasca)) throw new ErrorDeValidacio("Només s'admeten instàncies de Tasca.", 'tasca', tasca);
    if (this.cercarPerId(tasca.id)) throw new ErrorDeValidacio(`Id duplicat: ${tasca.id}.`, 'id', tasca.id);   // R1
    this.#comprovarCarrega(tasca);                                                                             // R7
    this.#tasques.push(tasca);
    return this;
  }

  eliminar(id) {
    const i = this.#tasques.findIndex((t) => t.id === id);
    if (i === -1) throw new ErrorDeValidacio(`No existeix la tasca ${id}.`, 'id', id);
    return this.#tasques.splice(i, 1)[0];
  }

  canviarEstat(id, nou) {
    const tasca = this.cercarPerId(id);
    if (tasca === null) throw new ErrorDeValidacio(`No existeix la tasca ${id}.`, 'id', id);
    tasca.canviarEstat(nou);
    return this;
  }

  // ── Mecanisme intern ────────────────────────────────────────────
  #comprovarCarrega(tasca) {
    if (!tasca.oberta || tasca.responsable === null) return;
    const actual = this.horesPerResponsable()[tasca.responsable] ?? 0;
    if (actual + tasca.horesEstimades > 40) {
      throw new ErrorDeValidacio(
        `${tasca.responsable} arribaria a ${actual + tasca.horesEstimades} h (màxim 40).`, 'horesEstimades', tasca.horesEstimades);
    }
  }
}

En ús, amb el backlog canònic:

const tauler = new Tauler('Taller Nómada', dadesBacklog.map((d) => new Tasca(d)));

console.log(tauler.resum('2026-09-20'));
// { total: 6, obertes: 5, horesTotals: 48, horesObertes: 45, vencudes: 1, esforc: 124 }
console.log(tauler.horesPerResponsable());      // { 'Iván': 25, 'Marta': 6, 'Lucía': 14 }

// Els intents de rodejar l'API:
tauler.tasques.push('això no és una tasca');
console.log(tauler.total);                      // 6   ← es va mutar una còpia, el tauler no se'n va assabentar

try { tauler.afegir('això tampoc'); }
catch (e) { console.log('✓ bloquejat:', e.message); }   // Només s'admeten instàncies de Tasca.

El criteri de disseny, resumit en una taula:

S'exposa No s'exposa Motiu
nom, total, resum() L'array #tasques real És l'invariant que cal protegir
tasques com a còpia Permet recórrer sense permetre mutar
afegir, eliminar, canviarEstat #comprovarCarrega És mecanisme, no interfície
filtrar(predicat) Dona flexibilitat total sense obrir l'array

Dos matisos honestos. El getter tasques copia l'array, però les tasques de dins són les mateixes instàncies: és una còpia superficial (04-08), així que qui la rebi sí que pot cridar tasca.canviarEstat(...). Això és intencionat —la tasca ja protegeix el seu propi estat—, però convé saber-ho. I filtrar(predicat) és una porta deliberadament oberta: en lloc d'escriure vint mètodes de consulta, es deixa que el consumidor porti el seu propi predicat, que és exactament la funció d'ordre superior de 03-06.

  1. El que és privat i JSON.stringify: el rescat de toJSON

Hi ha un preu a pagar per tota aquesta protecció, i apareix així que intentes desar les dades.

const tasca = new Tasca({
  id: 6, titol: 'Pressupost de la fusteria', responsable: 'Iván',
  prioritat: 'alta', etiquetes: ['fusteria', 'compres'],
  horesEstimades: 5, dataLimit: '2026-09-05', revisor: 'Marta'
});

console.log(JSON.stringify(tasca));
// {"id":6,"titol":"Pressupost de la fusteria","responsable":"Iván","prioritat":"alta","dataLimit":"2026-09-05","revisor":"Marta"}

Falten estat, horesEstimades i etiquetes. El motiu el saps des de 04-08: JSON.stringify recorre les propietats pròpies enumerables, i els camps privats no ho són. Els getters tampoc no compten: viuen al prototip, i stringify no mira la cadena. El resultat és un JSON silenciosament incomplet, que és el pitjor tipus de fallada possible: no llança cap error i només es descobreix en importar les dades.

La solució és el mètode toJSON que vas conèixer a 04-08: si un objecte el té, JSON.stringify fa servir el seu valor de retorn en lloc de l'objecte.

class Tasca {
  // …tot l'anterior…

  /** Representació serialitzable: inclou el que és privat com a camps públics. */
  toJSON() {
    return {
      id: this.id,
      titol: this.titol,
      responsable: this.responsable,
      prioritat: this.prioritat,
      estat: this.#estat,
      etiquetes: [...this.#etiquetes],
      horesEstimades: this.#hores,
      dataLimit: this.dataLimit,
      revisor: this.revisor
    };
  }

  /** Camí invers: reconstrueix una instància a partir de dades planes. */
  static desDeJSON(dades) {
    return new Tasca(typeof dades === 'string' ? JSON.parse(dades) : dades);
  }
}

console.log(JSON.stringify(tasca));
// {"id":6,…,"estat":"pendent","etiquetes":["fusteria","compres"],"horesEstimades":5,…}

I al tauler, el cicle complet d'exportació i importació de 04-08, ara amb classes:

class Tauler {
  // …tot l'anterior…

  toJSON() {
    return { versio: 1, nom: this.#nom, tasques: this.#tasques };   // stringify crida el toJSON de cada tasca
  }

  static importar(text) {
    let dades;
    try {
      dades = JSON.parse(text);
    } catch (error) {
      throw new ErrorDeDades('El backlog no és JSON vàlid.', error);
    }
    if (dades.versio !== 1) throw new ErrorDeDades(`Versió no admesa: ${dades.versio}.`);
    return new Tauler(dades.nom, dades.tasques.map((d) => Tasca.desDeJSON(d)));
  }
}

const text = JSON.stringify(tauler);
const recuperat = Tauler.importar(text);

console.log(recuperat.resum('2026-09-20'));
// { total: 6, obertes: 5, horesTotals: 48, horesObertes: 45, vencudes: 1, esforc: 124 }
console.log(recuperat.cercarPerId(6) instanceof Tasca);   // true  ← instàncies de debò

El parell toJSON / desDeJSON és el patró que has de recordar: una sortida i una entrada explícites, controlades per la classe, que a més revaliden en importar. Sense ell, tot el que és privat es perd en desar.

Un avís final sobre structuredClone, que vas estudiar a 04-08: no funciona amb camps privats. Llança un DataCloneError perquè no pot reconstruir la classe. La còpia profunda d'una instància es fa amb Tasca.desDeJSON(tasca.toJSON()), o amb un mètode clonar() que faci exactament això.

Errors Habituals i Consells

  • Recursió infinita al setter. set estat(v) { this.estat = v; } es crida a si mateix fins a esgotar la pila (el RangeError de 03-05). El magatzem intern ha de tenir un altre nom: #estat.
  • Validar només al setter i saltar-se'l al constructor. Si el constructor escriu this.#hores = dades.hores en lloc de this.horesEstimades = dades.hores, la validació no s'aplica en la creació. Assigna sempre a través de la propietat pública.
  • Fer servir #camp des d'una subclasse. Els privats no s'hereten: TascaRecurrent no pot llegir this.#estat. Si una subclasse necessita accés, exposa un getter (o protected per convenció amb _, que JavaScript no té com a mecanisme).
  • Oblidar enumerable: true a Object.defineProperty. Per defecte és false, i la propietat desapareix d'Object.keys i de JSON.stringify sense avisar.
  • Getters cars. Un getter que ordena o recorre milers d'elements enganya qui el llegeix. Si costa, que sigui un mètode amb parèntesis.
  • Getters amb efectes secundaris. Un getter que modifica l'objecte, incrementa un comptador o llança errors en condicions normals trenca l'expectativa que llegir una propietat és innocu, i fa la depuració molt difícil (la mateixa consola del navegador executa els getters en inspeccionar un objecte).
  • Retornar l'array intern des d'un getter. get etiquetes() { return this.#etiquetes; } deixa la porta oberta a tasca.etiquetes.push(…). Retorna una còpia si l'array ha de mantenir invariants.
  • Encapsular per encapsular. Un objecte de dades pures —una fila llegida d'una API, un punt {x, y}— no hi guanya res amb quinze getters trivials. L'encapsulació és per a objectes que tenen regles a protegir.
  • Consell: per decidir què fer privat, escriu primer com vols que es faci servir l'objecte des de fora. Tot el que no aparegui en aquelles línies d'ús és candidat a ser privat.

Exercicis

Exercici 1 — Resident amb quota d'hores. Escriu una classe Resident per al coworking de Taller Nómada amb:

  • camps privats #horesReservades (inicial 0) i #pla;
  • nom de només lectura mitjançant Object.defineProperty (R1 aplicada a persones);
  • getters pla, horesReservades, horesDisponibles i alDia (true si li queden hores);
  • un setter pla que només accepti 'mensual' (40 h) o 'diari' (8 h) i que rebutgi un canvi de pla si les hores ja reservades superarien la quota del pla nou;
  • un mètode reservar(hores) que validi i sumi, llançant ErrorDeValidacio si excedeix la quota.

Exercici 2 — De _ a #. Converteix aquesta classe heretada a camps privats, getters i setters, i localitza la fallada d'encapsulació que el guió baix deixava passar.

class Pressupost {
  constructor(limit) {
    this._limit = limit;
    this._linies = [];
  }
  get total() { return this._linies.reduce((s, l) => s + l.quantitat, 0); }
  get linies() { return this._linies; }
  afegir(concepte, quantitat) {
    if (this.total + quantitat > this._limit) throw new Error('Es supera el límit.');
    this._linies.push({ concepte, quantitat });
  }
}

Exercici 3 — Serialització amb privats. Partint de la classe Tasca de l'apartat 6, afegeix toJSON(), static desDeJSON(dades) i un mètode clonar(). Demostra amb codi que: (a) sense toJSON es perden tres camps; (b) amb toJSON el viatge d'anada i tornada ho conserva tot; (c) el clon és independent de l'original —canviar l'estat d'un no afecta l'altre— i (d) structuredClone falla.

Solucions

Exercici 1

'use strict';

class Resident {
  static #QUOTES = { mensual: 40, diari: 8 };

  #pla;
  #horesReservades = 0;

  constructor(nom, pla) {
    Object.defineProperty(this, 'nom', { value: nom, writable: false, enumerable: true, configurable: false });
    this.pla = pla;                                // passa pel setter: valida
  }

  get pla()               { return this.#pla; }
  get horesReservades()   { return this.#horesReservades; }
  get quota()             { return Resident.#QUOTES[this.#pla]; }
  get horesDisponibles()  { return this.quota - this.#horesReservades; }
  get alDia()             { return this.horesDisponibles > 0; }

  set pla(nou) {
    if (!Object.hasOwn(Resident.#QUOTES, nou)) {
      throw new ErrorDeValidacio(`Pla desconegut: "${nou}".`, 'pla', nou);
    }
    if (this.#horesReservades > Resident.#QUOTES[nou]) {
      throw new ErrorDeValidacio(
        `No es pot passar a "${nou}": ja hi ha ${this.#horesReservades} h reservades i la quota és ${Resident.#QUOTES[nou]}.`,
        'pla', nou);
    }
    this.#pla = nou;
  }

  reservar(hores) {
    if (typeof hores !== 'number' || hores <= 0) {
      throw new ErrorDeValidacio('Les hores han de ser un nombre positiu.', 'hores', hores);
    }
    if (this.#horesReservades + hores > this.quota) {
      throw new ErrorDeValidacio(
        `${this.nom} només té ${this.horesDisponibles} h disponibles.`, 'hores', hores);
    }
    this.#horesReservades += hores;
    return this;
  }
}

const lucia = new Resident('Lucía', 'mensual');
lucia.reservar(14).reservar(12);
console.log(lucia.horesReservades, lucia.horesDisponibles, lucia.alDia);   // 26 14 true

try { lucia.pla = 'diari'; }
catch (e) { console.error(e.message); }   // No es pot passar a "diari": ja hi ha 26 h reservades i la quota és 8.

try { lucia.nom = 'Lucia'; }
catch (e) { console.error(e.constructor.name); }   // TypeError

try { lucia.reservar(20); }
catch (e) { console.error(e.message); }   // Lucía només té 14 h disponibles.

El detall interessant és el setter de pla: no valida només el valor entrant, sinó la coherència amb l'estat actual. Aquesta és la mena d'invariant que només es pot protegir des de dins de l'objecte, perquè només ell coneix les dues peces de la dada. I observa que el constructor assigna this.pla = pla en lloc de this.#pla = pla: una regla, un lloc.

Exercici 2

La fallada era al getter linies: retornava l'array intern, així que qualsevol podia saltar-se completament la comprovació del límit.

const p = new Pressupost(500);
p.afegir('Tinta serigràfica', 120);
p.linies.push({ concepte: 'Torn nou', quantitat: 9000 });   // ✗ ningú no comprova res
console.log(p.total);                                        // 9120, amb límit 500

Versió corregida:

'use strict';

class Pressupost {
  #limit;
  #linies = [];

  constructor(limit) {
    if (typeof limit !== 'number' || limit <= 0) {
      throw new ErrorDeValidacio('El límit ha de ser un nombre positiu.', 'limit', limit);
    }
    this.#limit = limit;
  }

  get limit()      { return this.#limit; }
  get total()      { return this.#linies.reduce((s, l) => s + l.quantitat, 0); }
  get disponible() { return this.#limit - this.total; }
  get linies()     { return this.#linies.map((l) => ({ ...l })); }   // còpia de 2 nivells

  afegir(concepte, quantitat) {
    if (this.total + quantitat > this.#limit) {
      throw new ErrorDeValidacio(
        `Es supera el límit: queden ${this.disponible} € i se'n demanen ${quantitat} €.`, 'quantitat', quantitat);
    }
    this.#linies.push({ concepte, quantitat });
    return this;
  }
}

const p = new Pressupost(500);
p.afegir('Tinta serigràfica', 120).afegir('Paper per enquadernar', 80);
p.linies.push({ concepte: 'Torn nou', quantitat: 9000 });   // muta una còpia

console.log(p.total, p.disponible);      // 200 300   ← el pressupost no se'n va assabentar
try { p.afegir('Torn nou', 9000); }
catch (e) { console.error(e.message); }  // Es supera el límit: queden 300 € i se'n demanen 9000 €.

Fixa't en get linies(): no n'hi ha prou amb [...this.#linies], perquè això copiaria l'array però compartiria els objectes de dins —la còpia superficial de 04-08—, i algú podria fer p.linies[0].quantitat = 9000. Per això es fa servir map((l) => ({ ...l })), que copia també cada línia. És la profunditat justa: un nivell més perquè les línies són objectes plans.

Exercici 3

'use strict';

class Tasca {
  // …camps privats i mètodes de l'apartat 6…

  toJSON() {
    return {
      id: this.id, titol: this.titol, responsable: this.responsable,
      prioritat: this.prioritat, estat: this.#estat,
      etiquetes: [...this.#etiquetes], horesEstimades: this.#hores,
      dataLimit: this.dataLimit, revisor: this.revisor
    };
  }

  static desDeJSON(dades) {
    return new Tasca(typeof dades === 'string' ? JSON.parse(dades) : dades);
  }

  clonar() {
    return Tasca.desDeJSON(this.toJSON());
  }
}

const original = new Tasca({
  id: 1, titol: 'Redissenyar la sala polivalent', responsable: 'Iván',
  prioritat: 'alta', estat: 'en-curs', etiquetes: ['espai', 'disseny'],
  horesEstimades: 12, dataLimit: '2026-09-30', revisor: 'Marta'
});

// (a) Sense toJSON es perdrien tres camps. Simulant-ho amb el spread:
console.log(Object.keys({ ...original }));
// [ 'id', 'titol', 'responsable', 'prioritat', 'dataLimit', 'revisor' ]
// falten: estat, etiquetes, horesEstimades

// (b) Amb toJSON, anada i tornada complet
const text = JSON.stringify(original);
const tornada = Tasca.desDeJSON(text);
console.log(tornada.estat, tornada.horesEstimades, tornada.etiquetes);
// 'en-curs' 12 [ 'espai', 'disseny' ]
console.log(tornada instanceof Tasca);         // true

// (c) El clon és independent
const copia = original.clonar();
copia.canviarEstat('feta');
console.log(original.estat, copia.estat);      // 'en-curs' 'feta'
copia.etiquetes = ['espai'];
console.log(original.etiquetes);               // [ 'espai', 'disseny' ]  ← intactes

// (d) structuredClone no pot amb els privats
try { structuredClone(original); }
catch (e) { console.error(e.name); }           // 'DataCloneError'

Tres conclusions d'aquest exercici. La primera, que el spread {...tasca} de 04-07 no veu el que és privat, exactament igual que JSON.stringify: la raó és la mateixa, tots dos operen sobre propietats pròpies enumerables. La segona, que clonar() implementat com a desDeJSON(toJSON()) és una còpia profunda que a més revalida: si l'original tingués dades corruptes, el clon fallaria en construir-se, cosa que és un avantatge, no un inconvenient. I la tercera, que structuredClone deixa de ser una opció així que hi ha classes amb privats, cosa que col·loca el parell toJSON/desDeJSON com el mecanisme de còpia i persistència oficial del projecte.

Conclusió

Has convertit una recomanació en una barrera. En començar la lliçó, tasca.estat = 'feta' i tasca.horesEstimades = 500 travessaven tota la validació del 05-02 sense resistència; ara una llança TypeError perquè la propietat només té getter, i l'altra llança ErrorDeValidacio des del setter. L'estat real viu a #estat, #hores i #etiquetes, camps que ni tan sols es poden anomenar des de fora de la classe —l'error és sintàctic, no d'execució—, i les regles R2, R3, R5, R6, R8 i R9 estan escrites cadascuna en un sol lloc, en el camí pel qual obligatòriament passa qualsevol valor.

Tens les quatre eines i saps quan fer servir cadascuna. get per a propietats calculades que mai no es dessincronitzen (oberta, diesRestants, horesObertes, esforc), amb el criteri de reservar-lo per al que és barat i sense arguments i deixar els mètodes per a les accions i els càlculs cars. set per interceptar les assignacions i validar abans de guardar, recordant sempre que el magatzem intern s'ha de dir diferent per no caure en la recursió infinita, i que el constructor ha d'assignar a través del setter per no duplicar la regla. #camp, #metode() i static #camp per a la privacitat real, amb el brand check #estat in objecte com a comprovació d'autenticitat que instanceof no pot oferir. I Object.defineProperty amb writable, enumerable i configurable per a les propietats de només lectura com l'id, sabent que els tres atributs valen false per defecte i que aquí hi ha l'origen de la meitat de les sorpreses.

Saps a més d'on ve tot això: la convenció _camp que no protegia res, els closures del patró mòdul de 03-04 que sí que protegien però renunciaven al prototip, i el WeakMap extern de les llibreries anteriors al 2022. Has dissenyat l'API pública de Tauler decidint conscientment què es veu —resum, horesPerResponsable, filtrar, afegir, canviarEstat— i què s'amaga —l'array #tasques i el mètode #comprovarCarrega—, amb còpies defensives als getters perquè ni tan sols un push sobre el que es retorna pugui trencar els invariants. I has pagat i resolt el preu de la privacitat: com que els camps # són invisibles a JSON.stringify, al spread i a structuredClone, el parell toJSON / desDeJSON es converteix en la porta oficial d'entrada i sortida de dades, amb revalidació inclosa i amb el resum canònic intacte després del viatge: 48 h, 45 obertes, 1 vençuda, esforç 124.

El model de Nómada Tasques ja és complet i protegit. Però continua vivint sencer en un sol fitxer: Tasca, TascaRecurrent, Tauler, ErrorDeValidacio, les constants, el backlog d'exemple i el codi que l'executa, tot barrejat i creixent sense fre. Amb <script> solts, a més, cada nom que declares competeix amb tots els altres al mateix espai global, i l'ordre de càrrega es converteix en una dependència implícita que ningú no ha escrit enlloc. Ordenar això —separar el projecte en fitxers amb fronteres explícites, que declaren què exposen i què necessiten— és el tema de Mòduls i Importació/Exportació.

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