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
- La mateixa classe, en les dues sintaxis
constructori mètodes d'instància- Camps de classe
- Mètodes i camps
static - La classe
Tascadel projecte extendsisuper: herència sense cerimònia- Sobreescriptura i polimorfisme
- La classe
Tauler - Els quatre pilars de la POO
- Composició davant d'herència
class ErrorDeValidacio extends Error, explicat del tot- Detalls del llenguatge que les classes canvien
- Errors Habituals i Consells
- Exercicis
- Conclusió
- 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')); // trueUna
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 dinsA la pràctica gairebé sempre es fa servir la declaració.
constructor i mètodes d'instància
constructor i mètodes d'instànciaEl 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
staticde 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
functionni 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
- 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 prototipEls 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.
- Mètodes i camps
static
staticUn 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ànciaAquesta ú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); // 7Aquest 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.
- La classe
Tasca del projecte
Tasca del projecteAmb 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.
extends i super: herència sense cerimònia
extends i super: herència sense cerimòniaTota 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 classessuper 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.
- 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
ifque 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.
- La classe
Tauler
TaulerAra 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.
}
- 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.
- 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.
class ErrorDeValidacio extends Error, explicat del tot
class ErrorDeValidacio extends Error, explicat del totFas 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:
extends ErrorposaError.prototypea la cadena, d'on vénenmessage,name,toStringi (als motors actuals)stack. Per això l'error s'imprimeix bé a la consola i produeix una traça com les de 03-05.super(missatge)executa el constructor d'Error, que és qui assignathis.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 ambextendscal cridarsuperabans de tocarthis.this.name = 'ErrorDeValidacio'ombreja (05-01) la propietatnameheretada d'Error.prototype, que val'Error'. És el que fa que la consola imprimeixi el nom correcte.- 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); // trueUn catch que comprovi instanceof ErrorDeValidacio capturarà també els ErrorDeRegla, sense tocar ni una línia. Això és polimorfisme aplicat a la gestió d'errors.
- 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ó:
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:
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 enumerableUn 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
classno se separen amb comes, a diferència dels objectes literals del Mòdul 4.class A { m1() {}, m2() {} }és unSyntaxError. - Oblidar
super(...)al constructor del fill, o fer servirthisabans de cridar-lo. ElReferenceErrorés explícit, però el motiu desconcerta si no recordes que éssuperqui inicialitza la part del pare. - Escriure
functionals mètodes.class A { function m() {} }no és vàlid: la sintaxi és l'abreujada de 04-02. - Definir un mètode amb nom
constructoresperant sobrecarregar. Només hi ha un constructor; per a diverses formes de crear, fes servir mètodesstaticde 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 ambsuper. Té un ús legítim —fixathisper 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
staticamb «compartit per les instàncies». Un membrestaticno és accessible des d'una instància:tasca.MAX_HORESésundefined; cal escriureTasca.MAX_HORES(othis.constructor.MAX_HORESsi 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} dadessobre 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 unaTascai 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 12Els 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 super —super(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
- Què és JavaScript?
- Configuració del teu Entorn de Desenvolupament
- El teu Primer Programa en JavaScript
- Sintaxi i Conceptes Bàsics de JavaScript
- Variables i Tipus de Dades
- Operadors Bàsics
- Conversió de Tipus i Comparacions
- El Projecte del Curs: Nómada Tasques
Mòdul 2: Estructures de Control
- Sentències Condicionals
- Bucles: for, while, do-while
- Sentències Switch
- Control del Flux: break, continue i Bucles Imbricats
- Gestió d'Errors amb try-catch
Mòdul 3: Funcions
- Definició i Crida de Funcions
- Expressions de Funció i Funcions Fletxa
- Paràmetres i Valors de Retorn
- Àmbit i Closures
- Hoisting i el Context d'Execució
- Funcions d'Ordre Superior
- Recursivitat
Mòdul 4: Objectes i Arrays
- Introducció als Objectes
- Mètodes d'Objecte i la Paraula Clau
this - Arrays: Conceptes Bàsics i Mètodes
- Iteració sobre Arrays
- Cercar, Ordenar i Agregar Dades: find, sort i reduce
- Desestructuració d'Arrays
- Desestructuració d'Objectes, Spread i Rest
- JSON i Còpies d'Objectes
Mòdul 5: Objectes i Funcions Avançades
- Prototips i Herència
- Classes i Programació Orientada a Objectes
- Encapsulació: Getters, Setters i Camps Privats
- Mòduls i Importació/Exportació
- JavaScript Asíncron: Callbacks
- Promeses i Async/Await
- El Bucle d'Esdeveniments i la Cua de Microtasques
- Iteradors i Generadors
Mòdul 6: El Model d'Objectes del Document (DOM)
- Introducció al DOM
- Selecció i Manipulació d'Elements del DOM
- Gestió d'Esdeveniments
- Propagació, Delegació i Esdeveniments Personalitzats
- Creació i Eliminació d'Elements del DOM
- Renderitzat de Llistes i Plantilles HTML
- Gestió i Validació de Formularis
Mòdul 7: APIs del Navegador i Temes Avançats
- Emmagatzematge Local i de Sessió
- Fetch API i AJAX
- Peticions Robustes: Errors, Timeouts i AbortController
- WebSockets
- Service Workers i Aplicacions Web Progressives (PWAs)
- APIs del Navegador Essencials
- Introducció a WebAssembly
Mòdul 8: Proves i Depuració
- Depuració de JavaScript
- Qualitat de Codi: ESLint, Prettier i Convencions
- Proves Unitàries amb Jest
- Dobles de Prova: Mocks, Stubs i Spies
- Proves d'Integració
- Proves d'Extrem a Extrem amb Cypress
Mòdul 9: Rendiment i Optimització
- Mesurar Abans d'Optimitzar: DevTools i Web Vitals
- Optimització del Rendiment de JavaScript
- Gestió de Memòria
- Manipulació Eficient del DOM
- Càrrega Diferida i Divisió de Codi
Mòdul 10: Frameworks i Llibreries de JavaScript
- Per Què Existeixen els Frameworks
- Introducció a React
- Gestió d'Estat amb Redux
- Conceptes Bàsics de Vue.js
- Conceptes Bàsics d'Angular
- Triar el Framework Adequat
