La lliçó anterior va acabar amb una herència funcionant, però també amb una llista d'incomoditats: tres línies de ritual (Pare.call(this, …), Object.create(Pare.prototype), restaurar constructor) per cada relació entre constructors, mètodes declarats lluny del constructor amb un Constructor.prototype.metode = function … per línia, i cap mecanisme que impedís oblidar el new. Des d'ES2015 el llenguatge ofereix la paraula clau class, que resol tot això sense canviar el mecanisme: per sota continuen sent funcions constructores i objectes prototip, exactament els del 05-01. En aquesta lliçó aprendràs la sintaxi completa —constructor, mètodes, camps, static, extends, super—, reescriuràs el model de Nómada Tasques com a class Tasca, class TascaRecurrent i class Tauler, veuràs els quatre pilars de la programació orientada a objectes aterrats en aquell codi, entendràs per què gairebé sempre convé compondre abans que heretar, i per fi comprendràs del tot aquell class ErrorDeValidacio extends Error que arrossegues com a recepta des de 02-05.

Contingut

  1. La mateixa classe, en les dues sintaxis
  2. constructor i mètodes d'instància
  3. Camps de classe
  4. Mètodes i camps static
  5. La classe Tasca del projecte
  6. extends i super: herència sense cerimònia
  7. Sobreescriptura i polimorfisme
  8. La classe Tauler
  9. Els quatre pilars de la POO
  10. Composició davant d'herència
  11. class ErrorDeValidacio extends Error, explicat del tot
  12. Detalls del llenguatge que les classes canvien
  13. Errors Habituals i Consells
  14. Exercicis
  15. Conclusió

  1. La mateixa classe, en les dues sintaxis

La millor manera de veure què aporta class és posar les dues versions una al costat de l'altra. A l'esquerra, el constructor de la lliçó anterior; a la dreta, el seu equivalent exacte.

// ── SINTAXI DE CONSTRUCTOR (05-01) ───────────────────────────────
function Tasca(dades) {
  this.id = dades.id;
  this.titol = dades.titol;
  this.estat = dades.estat ?? 'pendent';
}

Tasca.prototype.descripcio = function () {
  return `[${this.id}] ${this.titol}`;
};

Tasca.prototype.estaVencuda = function (avui) {
  return this.dataLimit < avui && this.estat !== 'feta';
};
// ── SINTAXI DE CLASSE (ES2015) ───────────────────────────────────
class Tasca {
  constructor(dades) {
    this.id = dades.id;
    this.titol = dades.titol;
    this.estat = dades.estat ?? 'pendent';
  }

  descripcio() {
    return `[${this.id}] ${this.titol}`;
  }

  estaVencuda(avui) {
    return this.dataLimit < avui && this.estat !== 'feta';
  }
}

I això és l'important: el resultat és el mateix mecanisme.

console.log(typeof Tasca);                                      // 'function'  ← continua sent una funció
const t = new Tasca({ id: 1, titol: 'Redissenyar la sala polivalent' });
console.log(Object.getPrototypeOf(t) === Tasca.prototype);      // true
console.log(Object.hasOwn(t, 'descripcio'));                    // false  ← viu al prototip
console.log(Object.hasOwn(Tasca.prototype, 'descripcio'));      // true

Una class és sucre sintàctic sobre les funcions constructores i els prototips. No és un sistema d'objectes diferent: és la mateixa cadena de prototips escrita de manera llegible, amb diverses correccions aplicades de sèrie.

Aquestes correccions, que anirem veient, són les d'aquesta taula:

Aspecte Constructor class
On es declaren els mètodes Solts, un per línia Dins del bloc, al costat del constructor
Els mètodes són enumerables? Sí (contaminen for...in) No, mai
Cridar sense new Silenciós o TypeError confús TypeError explícit i inevitable
Herència 3 línies de ritual extends
Cridar el pare Pare.prototype.m.call(this) super.m()
constructor correcte Cal restaurar-lo a mà Automàtic
Mode estricte Cal demanar-lo Sempre actiu dins de la classe
Hoisting Es pot fer servir abans de declarar-la No: hi ha TDZ, com amb let

Les classes també existeixen com a expressió, igual que les funcions de 03-02:

const Tasca = class { /* … */ };                  // expressió de classe anònima
const Tasca = class TascaInterna { /* … */ };     // amb nom, visible només a dins

A la pràctica gairebé sempre es fa servir la declaració.

  1. constructor i mètodes d'instància

El bloc constructor és la funció que s'executa en fer new. És el pas 3 dels quatre que vas desmuntar a 05-01: rep els arguments i omple l'objecte nou a través de this.

'use strict';

class Persona {
  constructor(nom, rol) {
    this.nom = nom;
    this.rol = rol;
  }

  presentar() {                            // mètode d'instància → va al prototip
    return `${this.nom} (${this.rol})`;
  }
}

const marta = new Persona('Marta', 'coordinadora');
console.log(marta.presentar());            // 'Marta (coordinadora)'

Regles del constructor:

  • Només n'hi pot haver un per classe. JavaScript no té sobrecàrrega de constructors; si necessites diverses maneres de construir, es fan servir mètodes static de fàbrica (apartat 4).
  • És opcional. Si no l'escrius, se'n fa servir un de buit per defecte (o un que crida super(...args) si la classe hereta).
  • No porta la paraula function ni coma al darrere. Dins del cos d'una classe, els membres se separen per salts de línia, no per comes: és un error molt típic en venir dels objectes literals del Mòdul 4.

Els mètodes d'instància s'escriuen amb la sintaxi abreujada que ja coneixes de 04-02, i —això és l'essencial— es col·loquen automàticament al prototip, així que totes les instàncies comparteixen una sola còpia:

const ivan = new Persona('Iván', 'dissenyador');
console.log(marta.presentar === ivan.presentar);   // true

  1. Camps de classe

A més del constructor, una classe pot declarar camps: propietats d'instància amb un valor inicial, escrites directament al cos.

'use strict';

class Comptador {
  valor = 0;                 // camp d'instància amb valor inicial
  historial = [];            // compte! vegeu la nota de sota

  incrementar() {
    this.valor++;
    this.historial.push(this.valor);
    return this.valor;
  }
}

const c1 = new Comptador();
const c2 = new Comptador();
c1.incrementar();
c1.incrementar();

console.log(c1.valor, c2.valor);           // 2 0
console.log(c1.historial, c2.historial);   // [1, 2]  []

Un camp s'executa una vegada per instància, just al principi del constructor. Per això c1.historial i c2.historial són arrays diferents: no es comparteix res. Això contrasta amb l'avís de 05-01 sobre no posar arrays al prototip, on sí que es compartirien. Els camps són propietats pròpies:

console.log(Object.hasOwn(c1, 'valor'));               // true   ← camp: propietat pròpia
console.log(Object.hasOwn(c1, 'incrementar'));         // false  ← mètode: al prototip

Els camps són especialment útils per a valors per defecte que no depenen dels arguments:

class Tasca {
  etiquetes = [];
  revisor = null;
  estat = 'pendent';           // R5: tota tasca neix pendent

  constructor(id, titol) {
    this.id = id;
    this.titol = titol;
  }
}
On declarar la propietat Quan
Camp (estat = 'pendent';) Valor inicial fix, independent dels arguments
Constructor (this.id = dades.id;) El valor ve dels arguments o requereix càlcul

Els camps s'inicialitzen abans del cos del constructor, així que aquest pot sobreescriure'ls sense problema.

  1. Mètodes i camps static

Un membre static pertany a la classe, no a les instàncies. S'hi accedeix com a Classe.membre, i dins d'un mètode estàtic this és la mateixa classe.

'use strict';

class Tasca {
  static ESTATS = ['pendent', 'en-curs', 'feta'];            // camp estàtic (constant compartida)
  static creades = 0;                                         // camp estàtic (comptador)

  constructor(id, titol) {
    this.id = id;
    this.titol = titol;
    Tasca.creades++;                                          // s'actualitza el comptador de la classe
  }

  static esEstatValid(estat) {                                // mètode estàtic
    return Tasca.ESTATS.includes(estat);
  }
}

new Tasca(1, 'Redissenyar la sala polivalent');
new Tasca(2, 'Cartelleria del taller de serigrafia');

console.log(Tasca.creades);                        // 2
console.log(Tasca.esEstatValid('en-curs'));        // true
console.log(Tasca.esEstatValid('cancellada'));     // false

const t = new Tasca(3, 'Actualitzar el web de reserves');
// console.log(t.esEstatValid('feta'));            // ✗ TypeError: no és un mètode d'instància

Aquesta última línia comentada és la diferència clau: els estàtics no s'hereten per les instàncies, viuen a l'objecte funció.

L'ús més valuós dels estàtics són les fàbriques: mètodes que construeixen instàncies de maneres alternatives, suplint la manca de sobrecàrrega de constructors. Reprenent el JSON de 04-08:

'use strict';

class Tasca {
  constructor(dades) {
    this.id = dades.id;
    this.titol = dades.titol;
    this.responsable = dades.responsable ?? null;
    this.prioritat = dades.prioritat ?? 'mitjana';
    this.estat = dades.estat ?? 'pendent';
    this.etiquetes = dades.etiquetes ?? [];
    this.horesEstimades = dades.horesEstimades;
    this.dataLimit = dades.dataLimit;
    this.revisor = dades.revisor ?? null;
  }

  /** Construeix una Tasca a partir d'un text JSON. */
  static desDeJSON(text) {
    let dades;
    try {
      dades = JSON.parse(text);
    } catch (error) {
      throw new Error(`JSON invàlid: ${error.message}`);
    }
    return new Tasca(dades);
  }

  /** Construeix una tasca ràpida amb els mínims imprescindibles. */
  static rapida(titol, responsable) {
    return new Tasca({ id: Tasca.seguentId(), titol, responsable, horesEstimades: 1, dataLimit: '2026-12-31' });
  }

  static ultimId = 6;                        // el backlog canònic arriba fins al 6
  static seguentId() {
    Tasca.ultimId += 1;
    return Tasca.ultimId;
  }
}

const desDeText = Tasca.desDeJSON('{"id":6,"titol":"Pressupost de la fusteria","horesEstimades":5,"dataLimit":"2026-09-05"}');
console.log(desDeText instanceof Tasca);     // true
console.log(desDeText.estat);                // 'pendent'  ← el valor per defecte s'aplica igual

const rapida = Tasca.rapida('Comprar tinta negra', 'Marta');
console.log(rapida.id);                      // 7

Aquest Tasca.seguentId() és el mateix generador d'identificadors que a 03-04 vas resoldre amb un closure. Aquí l'estat viu en un camp estàtic en lloc d'en una variable capturada; a 05-03 veuràs com protegir-lo perquè ningú no pugui escriure Tasca.ultimId = 0 des de fora.

  1. La classe Tasca del projecte

Amb la sintaxi coberta, aquesta és la versió definitiva del model, amb les regles de negoci del projecte a dins. Para especial atenció a canviarEstat, que aplica la R6 (només les transicions permeses).

'use strict';

const PESOS = { alta: 3, mitjana: 2, baixa: 1 };
const MARQUES = { pendent: '○', 'en-curs': '▸', feta: '✓' };

class Tasca {
  // ── Constants de la classe ──────────────────────────────────────
  static ESTATS = ['pendent', 'en-curs', 'feta'];
  static TRANSICIONS = {                        // R6: el diagrama d'estats del 01-08
    pendent: ['en-curs'],
    'en-curs': ['pendent', 'feta'],
    feta: []
  };
  static MAX_HORES = 40;                        // R3 i R7

  // ── Camps amb valor per defecte ─────────────────────────────────
  estat = 'pendent';                            // R5
  etiquetes = [];
  revisor = null;

  constructor(dades) {
    if (typeof dades.titol !== 'string' || dades.titol.trim() === '') {
      throw new ErrorDeValidacio('El títol no pot estar buit.', 'titol', dades.titol);   // R2
    }
    if (!(dades.horesEstimades > 0 && dades.horesEstimades <= Tasca.MAX_HORES)) {
      throw new ErrorDeValidacio(`Les hores han de ser entre 1 i ${Tasca.MAX_HORES}.`, 'horesEstimades', dades.horesEstimades);   // R3
    }

    this.id = dades.id;
    this.titol = dades.titol.trim();
    this.responsable = dades.responsable ?? null;                    // R8
    this.prioritat = dades.prioritat ?? 'mitjana';
    this.horesEstimades = dades.horesEstimades;
    this.dataLimit = dades.dataLimit;
    if (dades.estat !== undefined) this.estat = dades.estat;
    if (dades.etiquetes !== undefined) {
      this.etiquetes = [...new Set(dades.etiquetes.map((e) => e.trim().toLowerCase()))];   // R9
    }
    if (dades.revisor !== undefined) this.revisor = dades.revisor;
  }

  // ── Consultes ───────────────────────────────────────────────────
  estaVencuda(avui) {
    return this.dataLimit < avui && this.estat !== 'feta';           // R10
  }

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

  esforc() {
    return (PESOS[this.prioritat] ?? 0) * this.horesEstimades;
  }

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

  // ── Modificació amb regles ──────────────────────────────────────
  canviarEstat(nou) {
    if (!Tasca.ESTATS.includes(nou)) {
      throw new ErrorDeValidacio(`Estat desconegut: "${nou}".`, 'estat', nou);
    }
    const permesos = Tasca.TRANSICIONS[this.estat];
    if (!permesos.includes(nou)) {                                   // R6
      throw new ErrorDeValidacio(
        `Transició no permesa: "${this.estat}" → "${nou}". Permeses: ${permesos.join(', ') || 'cap'}.`,
        'estat', nou
      );
    }
    this.estat = nou;
    return this;                                                     // interfície fluida (04-02)
  }

  toJSON() {                                                         // reprenent 04-08
    return { ...this };
  }

  toString() {                                                       // reprenent 05-01
    return `Tasca#${this.id} «${this.titol}»`;
  }
}

I en ús:

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.etiquetes);                  // [ 'fusteria', 'compres' ]  ← R9 aplicada
console.log(fusteria.descripcio('2026-09-20'));   // ○ [6] Pressupost de la fusteria · Iván · 5 h ⚠ VENÇUDA
console.log(fusteria.esforc());                   // 15

fusteria.canviarEstat('en-curs');                 // ✓ pendent → en-curs
console.log(fusteria.estat);                      // 'en-curs'

try {
  const altra = new Tasca({ id: 9, titol: 'Nova', horesEstimades: 4, dataLimit: '2026-10-01' });
  altra.canviarEstat('feta');                     // ✗ pendent → feta no existeix (R6)
} catch (error) {
  console.error(`${error.name} a «${error.camp}»: ${error.message}`);
  // ErrorDeValidacio a «estat»: Transició no permesa: "pendent" → "feta". Permeses: en-curs.
}

El que has aconseguit amb això és enorme i convé dir-ho explícitament: ja no és possible tenir una tasca en un estat invàlid. Al Mòdul 4, qualsevol línia del programa podia escriure tasca.estat = 'feta' i saltar-se la R6. Ara existeix un mètode que és el camí oficial. Encara es pot esquivar escrivint directament a la propietat —aquest forat el tanques a 05-03—, però la intenció del disseny ja està expressada al codi.

  1. extends i super: herència sense cerimònia

Tota la cerimònia de l'apartat 12 de la lliçó anterior es redueix a una paraula.

'use strict';

class TascaRecurrent extends Tasca {
  static PERIODES = { setmanal: 7, quinzenal: 14, mensual: 30 };

  constructor(dades) {
    super(dades);                                  // ← equival a Tasca.call(this, dades)
    this.periodicitat = dades.periodicitat ?? 'setmanal';
    this.repeticions = 0;
  }

  properaData() {
    const dies = TascaRecurrent.PERIODES[this.periodicitat] ?? 7;
    const base = new Date(this.dataLimit);
    base.setDate(base.getDate() + dies);
    return base.toISOString().slice(0, 10);
  }

  descripcio(avui) {
    return `${super.descripcio(avui)} · es repeteix ${this.periodicitat}`;   // ← crida la versió del pare
  }

  canviarEstat(nou) {
    super.canviarEstat(nou);                       // valida amb les regles del pare
    if (nou === 'feta') {                          // …i hi afegeix el seu propi comportament
      this.repeticions += 1;
      this.dataLimit = this.properaData();
      this.estat = 'pendent';                      // una recurrent torna a començar
    }
    return this;
  }
}

const recollida = new TascaRecurrent({
  id: 8, titol: 'Recollir el material de serigrafia', responsable: 'Marta',
  prioritat: 'baixa', horesEstimades: 1, dataLimit: '2026-09-25', periodicitat: 'setmanal'
});

console.log(recollida.descripcio());
// ○ [8] Recollir el material de serigrafia · Marta · 1 h · es repeteix setmanal

recollida.canviarEstat('en-curs').canviarEstat('feta');
console.log(recollida.dataLimit, recollida.estat, recollida.repeticions);
// '2026-10-02' 'pendent' 1

console.log(recollida instanceof TascaRecurrent);   // true
console.log(recollida instanceof Tasca);            // true
console.log(TascaRecurrent.MAX_HORES);              // 40  ← els estàtics SÍ que s'hereten entre classes

super té dos usos, i convé no barrejar-los:

Forma On es fa servir Què fa
super(args) Només al constructor Executa el constructor de la classe pare sobre this
super.metode(args) En qualsevol mètode Crida la versió del pare del mètode, amb this intacte

I una regla que el llenguatge imposa amb duresa: en una classe amb extends, cal cridar super(...) abans de fer servir this.

class Trencada extends Tasca {
  constructor(dades) {
    this.extra = 1;      // ✗ ReferenceError: Must call super constructor… before accessing 'this'
    super(dades);
  }
}

El motiu és coherent amb el que saps de 05-01: és la crida a super la que crea i inicialitza l'objecte de la part pare. Abans d'ella, this literalment encara no existeix. Si no escrius cap constructor a la classe filla, JavaScript en genera un d'implícit equivalent a constructor(...args) { super(...args); }, que és justament el que vols el 80 % de les vegades.

La cadena resultant és idèntica a la que vas dibuixar a mà a 05-01, però muntada pel llenguatge:

flowchart TD
    R["recollida<br/>{ id: 8, periodicitat, repeticions… }"] -->|"[[Prototype]]"| TRP["TascaRecurrent.prototype<br/>{ properaData, descripcio, canviarEstat }"]
    TRP -->|"[[Prototype]]"| TP["Tasca.prototype<br/>{ estaVencuda, esforc, descripcio, canviarEstat… }"]
    TP -->|"[[Prototype]]"| OP["Object.prototype"]
    OP -->|"[[Prototype]]"| N["null"]
    TRC["TascaRecurrent<br/>(la classe)"] -.->|"[[Prototype]] · així s'hereten<br/>els membres static"| TC["Tasca<br/>(la classe)"]

Fixa't en la fletxa puntejada: extends enllaça dues cadenes, la dels prototips (per als mètodes d'instància) i la de les mateixes classes (per als membres static). Per això TascaRecurrent.MAX_HORES funciona sense haver-lo declarat.

  1. Sobreescriptura i polimorfisme

Quan la classe filla defineix un mètode amb el mateix nom que el pare, el sobreescriu: en buscar a la cadena es troba abans el del fill. Això ja ho vas veure. El que és nou és la conseqüència de disseny: el polimorfisme.

Polimorfisme: codi que crida el mateix mètode sobre objectes de classes diferents i obté el comportament adequat a cadascun, sense if que preguntin de quina classe es tracta.

'use strict';

class TascaBloquejada extends Tasca {
  constructor(dades) {
    super(dades);
    this.motiu = dades.motiu ?? 'sense especificar';
  }

  descripcio(avui) {
    return `⛔ ${super.descripcio(avui)} · BLOQUEJADA (${this.motiu})`;
  }

  canviarEstat() {
    throw new ErrorDeValidacio('Una tasca bloquejada no pot canviar el seu estat.', 'estat', this.estat);
  }
}

const barreja = [
  new Tasca({ id: 1, titol: 'Redissenyar la sala polivalent', responsable: 'Iván', prioritat: 'alta', estat: 'en-curs', horesEstimades: 12, dataLimit: '2026-09-30' }),
  new TascaRecurrent({ id: 8, titol: 'Recollir el material de serigrafia', responsable: 'Marta', prioritat: 'baixa', horesEstimades: 1, dataLimit: '2026-09-25' }),
  new TascaBloquejada({ id: 9, titol: 'Instal·lar el torn nou', responsable: 'Lucía', prioritat: 'mitjana', horesEstimades: 6, dataLimit: '2026-10-10', motiu: 'falta el permís del propietari' })
];

for (const tasca of barreja) {
  console.log(tasca.descripcio('2026-09-20'));       // ← la MATEIXA crida
}
// ▸ [1] Redissenyar la sala polivalent · Iván · 12 h
// ○ [8] Recollir el material de serigrafia · Marta · 1 h · es repeteix setmanal
// ⛔ ○ [9] Instal·lar el torn nou · Lucía · 6 h · BLOQUEJADA (falta el permís del propietari)

Aquell bucle no conté ni un sol if sobre el tipus. Compara'l amb l'alternativa que hauries escrit al Mòdul 2:

// ✗ Sense polimorfisme: cada classe nova obliga a tocar aquest if
for (const tasca of barreja) {
  if (tasca.tipus === 'recurrent') console.log(descripcioRecurrent(tasca));
  else if (tasca.tipus === 'bloquejada') console.log(descripcioBloquejada(tasca));
  else console.log(descripcioNormal(tasca));
}

La versió polimòrfica no es toca mai en afegir classes. Aquest és tot el guany, i és la raó principal per la qual existeix l'herència.

  1. La classe Tauler

Ara l'altre objecte gran del projecte: el tauler que a 04-02 era un objecte literal, convertit en classe. Reuneix el backlog i les operacions de 04-05 en una sola peça.

'use strict';

const AVUI = '2026-09-20';

class Tauler {
  constructor(nom, tasques = []) {
    this.nom = nom;
    this.tasques = [...tasques];      // còpia: ningú de fora no muta el nostre array (04-08)
  }

  // ── Consultes ───────────────────────────────────────────────────
  cercarPerId(id) {
    return this.tasques.find((t) => t.id === id) ?? null;
  }

  filtrar(predicat) {
    return this.tasques.filter(predicat);
  }

  obertes() {
    return this.tasques.filter((t) => t.estaOberta());
  }

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

  // ── Modificació ─────────────────────────────────────────────────
  afegir(tasca) {
    if (!(tasca instanceof Tasca)) {
      throw new ErrorDeValidacio('Només es poden afegir instàncies de Tasca.', 'tasca', tasca);
    }
    if (this.cercarPerId(tasca.id) !== null) {
      throw new ErrorDeValidacio(`Ja existeix una tasca amb id ${tasca.id}.`, 'id', tasca.id);   // R1
    }
    const carrega = this.horesPerResponsable()[tasca.responsable] ?? 0;
    if (tasca.estaOberta() && carrega + tasca.horesEstimades > Tasca.MAX_HORES) {                // R7
      throw new ErrorDeValidacio(
        `${tasca.responsable} arribaria a ${carrega + tasca.horesEstimades} h, per sobre del màxim de ${Tasca.MAX_HORES}.`,
        'horesEstimades', tasca.horesEstimades
      );
    }
    this.tasques.push(tasca);
    return this;
  }

  canviarEstat(id, nou) {
    const tasca = this.cercarPerId(id);
    if (tasca === null) throw new ErrorDeValidacio(`No hi ha cap tasca amb id ${id}.`, 'id', id);
    tasca.canviarEstat(nou);              // delega la R6 a la mateixa tasca (polimorfisme inclòs)
    return this;
  }

  // ── Agregació ───────────────────────────────────────────────────
  resum(avui = AVUI) {
    const obertes = this.obertes();
    return {
      total: this.tasques.length,
      obertes: obertes.length,
      horesTotals: this.tasques.reduce((s, t) => s + t.horesEstimades, 0),
      horesObertes: obertes.reduce((s, t) => s + t.horesEstimades, 0),
      vencudes: obertes.filter((t) => t.estaVencuda(avui)).length,
      esforc: this.tasques.reduce((s, t) => s + t.esforc(), 0)
    };
  }

  pintar(avui = AVUI) {
    return [`— ${this.nom} —`, ...this.tasques.map((t) => t.descripcio(avui))].join('\n');
  }
}

Amb el backlog canònic:

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

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

console.log(tauler.resum());
// { 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 cinc números canònics del projecte —48, 45, 1 vençuda, 124 d'esforç, i el repartiment Iván 25 / Lucía 14 / Marta 6— surten ara d'una sola crida a un objecte que sap respondre per si mateix. I la R7 funciona de debò:

try {
  tauler.afegir(new Tasca({ id: 10, titol: 'Muntar exposició de tardor', responsable: 'Iván',
                            prioritat: 'alta', horesEstimades: 20, dataLimit: '2026-10-30' }));
} catch (error) {
  console.error(error.message);
  // Iván arribaria a 45 h, per sobre del màxim de 40 h.
}

  1. Els quatre pilars de la POO

Amb Tasca, TascaRecurrent, TascaBloquejada i Tauler al davant, els conceptes clàssics de la programació orientada a objectes deixen de ser abstractes.

Pilar Què significa On és a Nómada Tasques
Abstracció Exposar què fa una cosa i amagar com Qui fa servir tauler.resum() no sap si per dins hi ha reduce, un bucle o una memòria cau
Encapsulació Les dades i les operacions que les governen viatgen juntes, i l'estat només es canvia per camins controlats canviarEstat aplica la R6; afegir aplica R1 i R7. Es completarà a 05-03 amb #estat
Herència Una classe reutilitza i especialitza el comportament d'una altra TascaRecurrent extends Tasca reaprofita validació, esforç i venciment
Polimorfisme La mateixa crida produeix el comportament propi de cada classe El bucle de l'apartat 7: un sol tasca.descripcio(avui) per a tres classes

Val la pena insistir en la diferència entre abstracció i encapsulació, perquè es confonen constantment. L'abstracció és una decisió de disseny de la interfície: quines operacions ofereixo. L'encapsulació és un mecanisme de protecció: què impedeixo que es toqui. Pots tenir una interfície ben abstreta i encapsulació nul·la —és exactament l'estat actual de Tasca, on canviarEstat existeix però ningú no impedeix tasca.estat = 'feta'—, i d'aquí que la lliçó següent estigui dedicada per complet a tancar aquell forat.

  1. Composició davant d'herència

L'herència és potent i per això se n'abusa. Imagina que la Marta demana tres coses més: que una tasca pugui ser exportable a CSV, que pugui notificar per correu i que pugui arxivar-se. Amb herència acabes en un arbre impossible: TascaRecurrentExportableNotificable. I si demà una tasca bloquejada també ha de ser exportable, no hi ha manera d'expressar-ho, perquè una classe només pot tenir un pare.

L'alternativa és la composició: en lloc de ser un, tenir un.

'use strict';

// ── Peces independents, reutilitzables per qualsevol classe ───────
class ExportadorCSV {
  constructor(columnes) {
    this.columnes = columnes;
  }
  linia(objecte) {
    return this.columnes.map((c) => String(objecte[c] ?? '')).join(';');
  }
  capcalera() {
    return this.columnes.join(';');
  }
}

class RegistreDeCanvis {
  constructor() { this.esdeveniments = []; }
  anotar(text, moment) { this.esdeveniments.push(`${moment} · ${text}`); }
  ultim() { return this.esdeveniments.at(-1) ?? null; }
}

// ── El tauler FA SERVIR aquestes peces, no les hereta ─────────────
class TaulerAuditat extends Tauler {
  constructor(nom, tasques) {
    super(nom, tasques);
    this.registre = new RegistreDeCanvis();                                     // composició
    this.exportador = new ExportadorCSV(['id', 'titol', 'responsable', 'estat', 'horesEstimades']);   // composició
  }

  canviarEstat(id, nou) {
    super.canviarEstat(id, nou);
    this.registre.anotar(`Tasca ${id} → ${nou}`, AVUI);
    return this;
  }

  aCSV() {
    return [this.exportador.capcalera(), ...this.tasques.map((t) => this.exportador.linia(t))].join('\n');
  }
}

const auditat = new TaulerAuditat('Taller Nómada', tauler.tasques);
auditat.canviarEstat(2, 'en-curs');
console.log(auditat.registre.ultim());       // '2026-09-20 · Tasca 2 → en-curs'
console.log(auditat.aCSV().split('\n')[1]);  // '1;Redissenyar la sala polivalent;Iván;en-curs;12'

ExportadorCSV i RegistreDeCanvis no saben res de tasques: serveixen igual per a un tauler, per a un llistat de residents o per a les reserves de la sala. Aquesta és la diferència:

Herència (extends) Composició (propietats)
Relació «és un» «té un»
Acoblament Fort: el fill depèn de la implementació del pare Feble: només depèn d'una interfície petita
Quantes se'n poden combinar Una sola classe pare Tantes peces com vulguis
Canviar en temps d'execució Impossible Trivial: substitueixes la peça
Risc típic Jerarquies profundes i fràgils Una mica més de codi per delegar

La regla que segueix gairebé tothom:

Fes servir herència quan el fill és realment un cas especial del pare i comparteix el seu contracte complet. Per a tota la resta, compon.

TascaRecurrent és herència legítima: és una tasca en tots els sentits, i on encaixi una Tasca hi encaixarà ella. ExportadorCSV no és una tasca ni un tauler: és una eina que es fa servir. I si dubtes, hi ha una prova pràctica: si en heretar et trobes sobreescrivint mètodes del pare perquè no facin res o perquè llancin errors —com va fer TascaBloquejada amb canviarEstat—, és un senyal que la relació «és un» no era tan neta com semblava.

  1. class ErrorDeValidacio extends Error, explicat del tot

Fas servir aquesta recepta des de 02-05 sense entendre-la. Ara ja tens totes les peces.

'use strict';

class ErrorDeValidacio extends Error {
  constructor(missatge, camp, valorRebut) {
    super(missatge);                      // 1 · el constructor de Error guarda el missatge
    this.name = 'ErrorDeValidacio';       // 2 · sobreescriu el name heretat ('Error')
    this.camp = camp;                     // 3 · dades pròpies del nostre error
    this.valorRebut = valorRebut;
  }

  descriure() {                           // 4 · res no impedeix afegir-hi mètodes
    return `[${this.name}] ${this.camp}: ${this.message} (rebut: ${JSON.stringify(this.valorRebut)})`;
  }
}

Línia per línia, amb el que has après:

  1. extends Error posa Error.prototype a la cadena, d'on vénen message, name, toString i (als motors actuals) stack. Per això l'error s'imprimeix bé a la consola i produeix una traça com les de 03-05.
  2. super(missatge) executa el constructor d'Error, que és qui assigna this.message. Sense aquella crida l'error existiria però amb el missatge buit —i a més el llenguatge ni tan sols et deixaria arribar-hi, perquè en una classe amb extends cal cridar super abans de tocar this.
  3. this.name = 'ErrorDeValidacio' ombreja (05-01) la propietat name heretada d'Error.prototype, que val 'Error'. És el que fa que la consola imprimeixi el nom correcte.
  4. Els camps extra (camp, valorRebut) són propietats pròpies normals i corrents: l'avantatge de tenir una classe d'error pròpia és precisament poder portar informació estructurada, en lloc de ficar-la al text del missatge.

I ara la part que fa que tot això valgui la pena: instanceof permet distingir tipus d'error al catch, cosa que a 02-05 feies comparant error.name amb un string.

class ErrorDeDades extends Error {                     // el de 04-08
  constructor(missatge, causa) {
    super(missatge);
    this.name = 'ErrorDeDades';
    this.causa = causa;
  }
}

function processar(text) {
  try {
    const tasca = Tasca.desDeJSON(text);
    tasca.canviarEstat('feta');
    return tasca;
  } catch (error) {
    if (error instanceof ErrorDeValidacio) {
      console.error(error.descriure());
      return null;                                     // recuperable: s'avisa l'usuari
    }
    if (error instanceof ErrorDeDades) {
      console.error(`Dades corruptes: ${error.message}`);
      return null;
    }
    throw error;                                       // desconegut: que pugi (fail-fast de 02-05)
  }
}

console.log(processar('{"id":11,"titol":"Vernissar les prestatgeries","horesEstimades":4,"dataLimit":"2026-10-08"}'));
// ErrorDeValidacio a… → null
// (pendent → feta no és una transició vàlida: R6)

error instanceof ErrorDeValidacio és més robust que error.name === 'ErrorDeValidacio' perquè comprova la cadena real de prototips, no una cadena de text que qualsevol podria escriure per accident. I com que l'herència funciona, pots crear famílies d'errors:

class ErrorDeRegla extends ErrorDeValidacio {
  constructor(missatge, camp, valorRebut, regla) {
    super(missatge, camp, valorRebut);
    this.name = 'ErrorDeRegla';
    this.regla = regla;                                // 'R6', 'R7'…
  }
}

const e = new ErrorDeRegla('Transició no permesa.', 'estat', 'feta', 'R6');
console.log(e instanceof ErrorDeRegla);        // true
console.log(e instanceof ErrorDeValidacio);    // true  ← un catch genèric també l'atrapa
console.log(e instanceof Error);               // true

Un catch que comprovi instanceof ErrorDeValidacio capturarà també els ErrorDeRegla, sense tocar ni una línia. Això és polimorfisme aplicat a la gestió d'errors.

  1. Detalls del llenguatge que les classes canvien

Quatre diferències de comportament que convé tenir clares.

Les classes no s'eleven (hoisting). A diferència de les declaracions de funció de 03-05, una classe està a la zona morta temporal fins a la seva declaració:

// new Abans();                  // ✗ ReferenceError: Cannot access 'Abans' before initialization
class Abans {}

Dins d'una classe sempre regeix el mode estricte, encara que el fitxer no porti 'use strict'. És una d'aquelles correccions de franc.

No es pot cridar sense new, ni per accident:

// Tasca({ id: 1 });     // ✗ TypeError: Class constructor Tasca cannot be invoked without 'new'

Els mètodes són no enumerables, així que ja no contaminen for...in, que era el problema de l'apartat 11 de 05-01:

class Exemple {
  camp = 1;
  metode() {}
}
const e = new Exemple();
for (const k in e) console.log(k);        // només 'camp'
console.log(Object.keys(Exemple.prototype));   // []  ← el mètode hi és, però no és enumerable

Un avís final que reprèn 04-08: JSON.parse no retorna instàncies. Si serialitzes un Tauler i el tornes a llegir, obtens objectes plans sense mètodes.

const copiaPlana = JSON.parse(JSON.stringify(tauler.tasques[0]));
console.log(copiaPlana.titol);                  // 'Redissenyar la sala polivalent'  ✓ dades
console.log(copiaPlana instanceof Tasca);       // false  ✗ sense comportament
// copiaPlana.esforc();                         // ✗ TypeError: no és una funció

La solució és la fàbrica estàtica de l'apartat 4: Tasca.desDeJSON (o dades.map((d) => new Tasca(d))) és el pas obligatori de rehidratació en llegir dades de fora. El mateix val per a structuredClone, que copia les dades però retorna un objecte pla.

Errors Habituals i Consells

  • Posar comes entre els membres de la classe. Dins de class no se separen amb comes, a diferència dels objectes literals del Mòdul 4. class A { m1() {}, m2() {} } és un SyntaxError.
  • Oblidar super(...) al constructor del fill, o fer servir this abans de cridar-lo. El ReferenceError és explícit, però el motiu desconcerta si no recordes que és super qui inicialitza la part del pare.
  • Escriure function als mètodes. class A { function m() {} } no és vàlid: la sintaxi és l'abreujada de 04-02.
  • Definir un mètode amb nom constructor esperant sobrecarregar. Només hi ha un constructor; per a diverses formes de crear, fes servir mètodes static de fàbrica.
  • Fer servir una funció fletxa com a mètode de classe quan esperes que vagi al prototip. descripcio = () => {…} és un camp d'instància, no un mètode: se'n crea una còpia per objecte (adeu a l'estalvi de memòria del 05-01) i no és sobreescrivible amb super. Té un ús legítim —fixa this per passar-lo com a callback, resolent el problema de 04-02—, però és una decisió conscient, no la manera normal d'escriure mètodes.
  • Confondre static amb «compartit per les instàncies». Un membre static no és accessible des d'una instància: tasca.MAX_HORES és undefined; cal escriure Tasca.MAX_HORES (o this.constructor.MAX_HORES si vols que les subclasses puguin redefinir-lo).
  • Heretar per reutilitzar codi. Si només vols aprofitar dues funcions, no creïs un pare artificial: importa aquelles funcions o compon. L'herència expressa una relació conceptual, no una drecera.
  • Jerarquies profundes. Més de dos o tres nivells fan impossible seguir d'on surt cada mètode. Quan la profunditat creix, gairebé sempre hi havia una composició esperant.
  • Consell: documenta les classes amb JSDoc com venies fent amb les funcions a 03-03. Un @param {Object} dades sobre el constructor li estalvia al teu editor —i a tu d'aquí a tres mesos— haver de llegir el cos sencer.

Exercicis

Exercici 1 — De constructor a classe. Tradueix a sintaxi class el codi heretat següent, corregint de passada els dos defectes que conté (un de memòria, un de dades compartides).

function Resident(nom, pla) {
  this.nom = nom;
  this.pla = pla;
  this.reserves = [];
  this.descriure = function () {
    return `${this.nom} · pla ${this.pla} · ${this.reserves.length} reserves`;
  };
}
Resident.prototype.tarifes = { mensual: 120, diari: 12 };
Resident.prototype.quota = function () {
  return this.tarifes[this.pla] ?? 0;
};

Exercici 2 — TascaAmbSubtasques. Escriu una classe que estengui Tasca i afegeixi un array subtasques d'instàncies de Tasca. Ha d'oferir:

  • afegirSubtasca(tasca), que valida que sigui una Tasca i l'afegeix;
  • horesTotals(), que sumi les seves hores i les de totes les seves subtasques a qualsevol profunditat (recursivitat de 03-07);
  • horesObertes(), igual però comptant només les que no estan fetes;
  • descripcio(avui) sobreescrit que afegeixi (+N subtasques) quan n'hi hagi.

Comprova-ho amb l'arbre de «Redissenyar la sala polivalent»: 12 h totals i 7 h obertes.

Exercici 3 — ErrorDeRegla amb catàleg. Escriu una classe ErrorDeRegla extends ErrorDeValidacio que rebi el codi de regla ('R6', 'R7'…) i exposi un mètode estàtic ErrorDeRegla.explicar(codi) que retorni el text de la regla a partir d'un catàleg estàtic. Fes-la servir a Tasca.canviarEstat i demostra que un catch (e) { if (e instanceof ErrorDeValidacio) … } la continua capturant.

Solucions

Exercici 1

'use strict';

class Resident {
  static TARIFES = { mensual: 120, diari: 12 };      // defecte 2 corregit: constant a la CLASSE

  constructor(nom, pla) {
    this.nom = nom;
    this.pla = pla;
    this.reserves = [];                              // un array NOU per instància
  }

  descriure() {                                      // defecte 1 corregit: mètode al prototip
    return `${this.nom} · pla ${this.pla} · ${this.reserves.length} reserves`;
  }

  quota() {
    return Resident.TARIFES[this.pla] ?? 0;
  }
}

const lucia = new Resident('Lucía', 'mensual');
const ivan = new Resident('Iván', 'diari');
console.log(lucia.descriure === ivan.descriure);     // true   ← una sola funció
console.log(lucia.quota(), ivan.quota());            // 120 12

Els dos defectes eren els de l'apartat 9 de 05-01. El primer, this.descriure = function … dins del constructor, creava una funció per instància; a la classe passa a ser un mètode del prototip. El segon, Resident.prototype.tarifes = {…}, posava un objecte mutable al prototip: qualsevol resident.tarifes.mensual = 0 ho hauria canviat per a tothom. Com a camp static continua sent compartit expressament, però s'hi accedeix per la classe, cosa que deixa clara la intenció (i a 05-03 veuràs com fer-lo a més de només lectura).

Exercici 2

'use strict';

class TascaAmbSubtasques extends Tasca {
  constructor(dades) {
    super(dades);
    this.subtasques = [];
    for (const sub of dades.subtasques ?? []) {
      this.afegirSubtasca(sub instanceof Tasca ? sub : new TascaAmbSubtasques(sub));
    }
  }

  afegirSubtasca(tasca) {
    if (!(tasca instanceof Tasca)) {
      throw new ErrorDeValidacio('Una subtasca ha de ser una Tasca.', 'subtasques', tasca);
    }
    this.subtasques.push(tasca);
    return this;
  }

  horesTotals() {
    return this.subtasques.reduce(
      (suma, sub) => suma + (sub instanceof TascaAmbSubtasques ? sub.horesTotals() : sub.horesEstimades),
      this.horesEstimades
    );
  }

  horesObertes() {
    const propies = this.estaOberta() ? this.horesEstimades : 0;
    return this.subtasques.reduce(
      (suma, sub) => suma + (sub instanceof TascaAmbSubtasques
        ? sub.horesObertes()
        : (sub.estaOberta() ? sub.horesEstimades : 0)),
      propies
    );
  }

  descripcio(avui) {
    const base = super.descripcio(avui);
    return this.subtasques.length > 0 ? `${base} (+${this.subtasques.length} subtasques)` : base;
  }
}

const redisseny = new TascaAmbSubtasques({
  id: 1, titol: 'Redissenyar la sala polivalent', responsable: 'Iván',
  prioritat: 'alta', estat: 'en-curs', horesEstimades: 0.0001, dataLimit: '2026-09-30',
  subtasques: [
    { id: 11, titol: 'Mesurar i aixecar el plànol', responsable: 'Iván', estat: 'feta', horesEstimades: 3, dataLimit: '2026-09-10' },
    { id: 12, titol: 'Triar el mobiliari', responsable: 'Marta', estat: 'en-curs', horesEstimades: 0.0001, dataLimit: '2026-09-20',
      subtasques: [
        { id: 121, titol: 'Demanar pressupostos', responsable: 'Marta', estat: 'feta', horesEstimades: 2, dataLimit: '2026-09-15' },
        { id: 122, titol: 'Visitar dos proveïdors', responsable: 'Marta', estat: 'pendent', horesEstimades: 2, dataLimit: '2026-09-22' }
      ] },
    { id: 13, titol: 'Pintar i muntar', responsable: 'Iván', estat: 'pendent', horesEstimades: 5, dataLimit: '2026-09-28' }
  ]
});

console.log(Math.round(redisseny.horesTotals()));    // 12
console.log(Math.round(redisseny.horesObertes()));   // 7   (2 de visitar + 5 de pintar)
console.log(redisseny.descripcio('2026-09-20'));
// ▸ [1] Redissenyar la sala polivalent · Iván · 0.0001 h (+3 subtasques)

Dues observacions honestes sobre aquesta solució. La primera, aquell horesEstimades: 0.0001 és un apedaçament lleig: els nodes contenidors de l'arbre de 03-07 tenien horesEstimades: 0, però la R3 que validem al constructor exigeix que siguin més grans que zero. És un xoc real entre dues regles, i la sortida neta seria permetre 0 per a les tasques contenidores —una excepció explícita a la validació— en lloc de falsejar la dada. Anota-ho com el que és: una decisió de disseny pendent. La segona, la recursió d'horesTotals és la mateixa de sumarHoresTotals de 03-07, ara convertida en mètode; el cas base continua sent implícit, perquè un array de subtasques buit fa que reduce retorni directament el valor inicial.

Exercici 3

'use strict';

class ErrorDeRegla extends ErrorDeValidacio {
  static CATALEG = {
    R1: "L'id és únic i correlatiu; l'assigna l'aplicació.",
    R2: 'El títol no pot estar buit.',
    R3: 'Les hores estimades han de ser entre 1 i 40.',
    R6: 'Només es permeten les transicions pendent → en-curs → feta.',
    R7: 'Ningú no pot superar 40 hores assignades en la mateixa setmana.'
  };

  constructor(codi, missatge, camp, valorRebut) {
    super(missatge, camp, valorRebut);
    this.name = 'ErrorDeRegla';
    this.regla = codi;
  }

  static explicar(codi) {
    return ErrorDeRegla.CATALEG[codi] ?? 'Regla desconeguda.';
  }

  descriure() {
    return `[${this.regla}] ${this.message} — ${ErrorDeRegla.explicar(this.regla)}`;
  }
}

// A Tasca.canviarEstat, substituint el throw de l'apartat 5:
//   throw new ErrorDeRegla('R6', `Transició no permesa: "${this.estat}" → "${nou}".`, 'estat', nou);

try {
  new Tasca({ id: 12, titol: 'Netejar la sala', horesEstimades: 2, dataLimit: '2026-10-01' })
    .canviarEstat('feta');
} catch (error) {
  console.log(error instanceof ErrorDeRegla);        // true
  console.log(error instanceof ErrorDeValidacio);    // true   ← el catch genèric l'atrapa
  console.log(error instanceof Error);               // true
  console.log(error.descriure());
  // [R6] Transició no permesa: "pendent" → "feta". — Només es permeten les transicions pendent → en-curs → feta.
}

Fixa't en dues coses. descriure() està sobreescrit i fa servir el catàleg estàtic, mentre que la versió del pare continuava disponible amb super.descriure() si l'haguéssim volguda. I els tres instanceof en true són la demostració pràctica de per què es fa servir instanceof i no error.name: un gestor escrit fa mesos per a ErrorDeValidacio continua capturant aquesta classe nova sense modificar ni una línia.

Conclusió

Has canviat de sintaxi sense canviar de mecanisme. class no introdueix un sistema d'objectes nou: per sota continua havent-hi una funció constructora i un objecte prototype, exactament els del 05-01, com demostra que typeof Tasca sigui 'function' i que Object.getPrototypeOf(t) === Tasca.prototype. El que aporta és llegibilitat —constructor i mètodes junts en un bloc— i un grapat de correccions que ja no has de recordar: mètodes no enumerables, mode estricte automàtic, constructor ben apuntat, impossibilitat d'oblidar el new i extends en lloc de les tres línies de ritual.

Ja tens el vocabulari complet. El constructor rep els arguments i omple l'objecte; els mètodes d'instància viuen al prototip i es comparteixen; els camps són propietats pròpies amb valor inicial, ideals per a valors per defecte com estat = 'pendent'; i els membres static pertanyen a la classe, i serveixen per a constants (Tasca.ESTATS, Tasca.TRANSICIONS, Tasca.MAX_HORES), comptadors (Tasca.ultimId) i sobretot fàbriques com Tasca.desDeJSON, que supleixen la manca de sobrecàrrega de constructors i són el pas obligatori per rehidratar les dades planes que retorna JSON.parse. Amb extends i supersuper(dades) al constructor, super.metode() als mètodes— has muntat TascaRecurrent i TascaBloquejada, i has vist el polimorfisme funcionant: un bucle que crida tasca.descripcio(avui) sobre tres classes diferents i no conté ni un sol if sobre el tipus.

El model del projecte ja és al seu lloc. class Tasca valida al constructor (R2, R3, R8, R9), calcula esforç i venciment (R10) i protegeix les transicions d'estat amb canviarEstat (R6). class Tauler guarda el backlog, comprova identificadors duplicats (R1) i la càrrega màxima per persona (R7), i produeix d'una tirada el resum canònic: 48 h totals, 45 obertes, 1 vençuda, esforç 124, amb l'Iván en 25 h, la Lucía en 14 i la Marta en 6. Els quatre pilars de la POO han deixat de ser una llista de paraules per assenyalar línies concretes d'aquell codi, saps quan l'herència és legítima («és un») i quan convé compondre («té un»), amb ExportadorCSV i RegistreDeCanvis com a peces reutilitzables. I class ErrorDeValidacio extends Error ja no és una recepta copiada: entens què fa super(missatge), per què se sobreescriu name, d'on surten message i stack, i per què error instanceof ErrorDeValidacio és millor que comparar cadenes de text.

Queda, però, una promesa incomplerta. Has escrit canviarEstat perquè ningú no pugui saltar-se la R6… i qualsevol línia del programa continua podent escriure tasca.estat = 'feta' i deixar l'objecte en un estat impossible. El mateix amb tasca.horesEstimades = 500, que travessa la validació del constructor com si no existís, o amb Tasca.ultimId = 0, que trencaria la R1 per sempre. L'abstracció està ben dissenyada, però l'encapsulació encara és una convenció basada en la bona voluntat. Tancar aquell forat —amb get, set, camps privats #estat i propietats de només lectura— és el tema d'Encapsulació: Getters, Setters i Camps Privats.

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