La lliçó anterior va acabar amb una promesa incomplerta. Vas escriure canviarEstat perquè les transicions respectessin la regla R6, i tanmateix qualsevol línia del programa continua podent escriure tasca.estat = 'feta' i deixar l'objecte en un estat impossible. El mètode existeix, però no és obligatori: és una recomanació educada. Aquesta lliçó va de convertir aquella recomanació en una barrera real. Aprendràs a exposar propietats calculades amb get, a interceptar les escriptures amb set, a amagar l'estat de debò amb camps privats #camp —una característica del llenguatge, no una convenció—, a declarar propietats de només lectura amb Object.defineProperty, i a dissenyar una API pública mínima per a Tauler. I com que el que és privat interfereix amb la serialització que vas aprendre a 04-08, tancaràs amb toJSON perquè exportar el backlog continuï funcionant.
Contingut
- Què pot trencar un consumidor sense encapsulació
get: propietats calculadesset: validar en l'assignació- Quan un getter i quan un mètode
- Camps privats
#camp - Mètodes privats i estàtics privats
- L'operador
#camp in objecte - Les alternatives històriques:
_camp, closures iWeakMap - Propietats de només lectura i
Object.defineProperty - Dissenyar l'API pública de
Tauler - El que és privat i
JSON.stringify: el rescat detoJSON - Errors Habituals i Consells
- Exercicis
- Conclusió
- Què pot trencar un consumidor sense encapsulació
Comencem mesurant el mal. Aquesta és la Tasca del 05-02, amb tota la seva validació, i aquestes són quatre línies que la destrueixen sense que l'objecte protesti.
'use strict';
const tasca = new Tasca({
id: 6, titol: 'Pressupost de la fusteria', responsable: 'Iván',
prioritat: 'alta', horesEstimades: 5, dataLimit: '2026-09-05'
});
// ✗ 1 · Se salta la R6: de 'pendent' a 'feta' sense passar per 'en-curs'
tasca.estat = 'feta';
// ✗ 2 · Se salta la R3: hores fora del rang 1-40
tasca.horesEstimades = 500;
// ✗ 3 · Estat inexistent: ni tan sols és a Tasca.ESTATS
tasca.estat = 'arxivada';
// ✗ 4 · Se salta la R8: cadena buida en lloc de null
tasca.responsable = '';
console.log(tasca.descripcio('2026-09-20'));
// ? [6] Pressupost de la fusteria · · 500 hAquell ? de la marca i aquell buit on hauria d'anar el responsable són les conseqüències visibles. Les invisibles són pitjors: el resum del tauler ara diu que hi ha 543 hores obertes, l'informe de la Marta quadra malament, i quan algú depuri el problema d'aquí a tres setmanes no hi haurà manera de saber quina línia va fer l'assignació il·legal, perquè una assignació no deixa rastre en cap traça.
El diagnòstic tècnic és simple: estat, horesEstimades i responsable són propietats públiques normals, i a JavaScript una propietat pública és un contracte obert. L'encapsulació consisteix a reduir la superfície de contacte: deixar visible només allò que el consumidor necessita, i obligar que tot canvi passi per un mètode que sàpiga validar-lo.
flowchart LR
subgraph Sense["Sense encapsulació"]
C1["Codi extern"] -->|"tasca.estat = 'feta'"| E1["estat<br/>(públic)"]
C1 -->|"tasca.canviarEstat('feta')"| M1["canviarEstat()"]
M1 --> E1
end
subgraph Amb["Amb encapsulació"]
C2["Codi extern"] -.->|"✗ inaccessible"| E2["#estat<br/>(privat)"]
C2 -->|"tasca.canviarEstat('feta')"| M2["canviarEstat()<br/>valida R6"]
M2 --> E2
end
get: propietats calculades
get: propietats calculadesEl primer pas és més suau que els camps privats i ja resol molts casos: una propietat que no es guarda, es calcula.
'use strict';
class Tasca {
constructor(dades) {
this.id = dades.id;
this.titol = dades.titol;
this.estat = dades.estat ?? 'pendent';
this.horesEstimades = dades.horesEstimades;
this.dataLimit = dades.dataLimit;
}
get oberta() {
return this.estat !== 'feta';
}
get diesRestants() {
const limit = new Date(this.dataLimit);
const avui = new Date('2026-09-20');
return Math.round((limit - avui) / (1000 * 60 * 60 * 24));
}
}
const fusteria = new Tasca({ id: 6, titol: 'Pressupost de la fusteria', horesEstimades: 5, dataLimit: '2026-09-05' });
console.log(fusteria.oberta); // true ← sense parèntesis
console.log(fusteria.diesRestants); // -15 ← vençuda fa 15 diesLes dues característiques que defineixen un getter:
- Es llegeix com una propietat, sense parèntesis.
fusteria.oberta, nofusteria.oberta(). - S'executa cada vegada que es llegeix, així que el valor sempre està al dia. Si canvies
estat,obertacanvia sola: no hi ha cap dada duplicada per mantenir sincronitzada.
Aquest segon punt és l'argument fort. L'alternativa —guardar this.oberta = true al constructor— crea dues fonts de veritat que tard o d'hora es dessincronitzen.
// ✗ Dada derivada guardada: cal recordar-se d'actualitzar-la SEMPRE
this.oberta = dades.estat !== 'feta';
tasca.estat = 'feta';
console.log(tasca.oberta); // true ← mentidaUn getter també funciona en objectes literals, no només en classes, amb la mateixa sintaxi:
const equip = {
membres: ['Marta', 'Iván', 'Lucía'],
get mida() { return this.membres.length; }
};
console.log(equip.mida); // 3I al Tauler els getters brillen especialment, perquè gairebé tot el que es consulta és derivat:
class Tauler {
constructor(nom, tasques = []) {
this.nom = nom;
this.tasques = [...tasques];
}
get total() { return this.tasques.length; }
get obertes() { return this.tasques.filter((t) => t.oberta); }
get horesTotals() { return this.tasques.reduce((s, t) => s + t.horesEstimades, 0); }
get horesObertes() { return this.obertes.reduce((s, t) => s + t.horesEstimades, 0); }
get esforc() { return this.tasques.reduce((s, t) => s + t.esforc(), 0); }
}
console.log(tauler.total); // 6
console.log(tauler.horesTotals); // 48
console.log(tauler.horesObertes); // 45
console.log(tauler.esforc); // 124Fixa't en horesObertes: fa servir el getter obertes, que al seu torn recorre tasques. Els getters es poden encadenar entre si amb tota naturalitat, i el resultat es llegeix com una descripció de les dades, no com una seqüència de crides.
set: validar en l'assignació
set: validar en l'assignacióUn setter és l'altra meitat: una funció que s'executa quan algú assigna a la propietat. Rep exactament un paràmetre, el valor assignat.
'use strict';
class Tasca {
constructor(dades) {
this.id = dades.id;
this._hores = 0;
this.horesEstimades = dades.horesEstimades; // ← passa pel setter, es valida
}
get horesEstimades() {
return this._hores;
}
set horesEstimades(valor) {
if (typeof valor !== 'number' || Number.isNaN(valor)) {
throw new ErrorDeValidacio('Les hores han de ser un nombre.', 'horesEstimades', valor);
}
if (valor <= 0 || valor > 40) { // R3
throw new ErrorDeValidacio('Les hores han de ser entre 1 i 40.', 'horesEstimades', valor);
}
this._hores = valor;
}
}
const t = new Tasca({ id: 6, horesEstimades: 5 });
console.log(t.horesEstimades); // 5
t.horesEstimades = 8; // ✓ vàlid
console.log(t.horesEstimades); // 8
try {
t.horesEstimades = 500; // ✗ ara SÍ que salta
} catch (error) {
console.error(error.message); // 'Les hores han de ser entre 1 i 40.'
}
console.log(t.horesEstimades); // 8 ← l'objecte ha quedat intacteJa no hi ha manera de ficar 500 hores en una tasca, ni tan sols per assignació directa. Quatre detalls que cal tenir clars:
- El getter i el setter comparteixen nom, i aquell nom no pot ser el del camp on es guarda la dada. Si escrivissis
set horesEstimades(v) { this.horesEstimades = v; }provocaries una recursió infinita: l'assignació torna a cridar el setter. Per això el magatzem intern es diu diferent (_horesaquí, i#horesaixí que arribem a l'apartat 5). - Un setter sense getter fa que llegir la propietat retorni
undefined. Gairebé sempre es declaren tots dos. - Un getter sense setter converteix la propietat en només lectura: en mode estricte, assignar-li alguna cosa llança
TypeError. És un ús perfectament legítim. - Validar al constructor no n'hi ha prou. Fixa't que el constructor de dalt assigna
this.horesEstimades = dades.horesEstimadesen lloc de tocar_horesdirectament: així la validació del setter s'aplica també en la creació, i la regla viu escrita una sola vegada.
class NomesLectura {
#valor = 42;
get valor() { return this.#valor; }
}
const nl = new NomesLectura();
// nl.valor = 7; // ✗ TypeError: Cannot set property valor… which has only a getter
- Quan un getter i quan un mètode
Getters i mètodes fan coses semblants, i triar malament produeix APIs incòmodes. La regla pràctica:
| Fes servir un getter quan… | Fes servir un mètode quan… |
|---|---|
| Retorna una dada conceptualment pròpia de l'objecte | Fa una acció o un càlcul amb nom de verb |
| És barat (recórrer un array curt, una resta) | És car (recórrer milers d'elements, ordenar, clonar) |
| No rep arguments | Rep arguments |
| No té efectes secundaris ni llança errors en condicions normals | Pot modificar l'objecte o fallar |
| Llegir-lo dues vegades seguides dona el mateix | El resultat pot canviar entre crides |
Aplicat al projecte:
class Tasca {
get oberta() { … } // ✓ dada pròpia, barata, sense arguments
get diesRestants() { … } // ✓ derivat de dataLimit
estaVencuda(avui) { … } // ✓ mètode: rep un argument
canviarEstat(nou) { … } // ✓ mètode: és una ACCIÓ, i pot llançar
esforc() { … } // ✓ mètode: podria ser getter; es manté per coherència
}El criteri del cost mereix un avís, perquè és el que més sorprèn. Qui llegeix tauler.horesObertes dona per fet que és una lectura barata, i no s'ho pensa dues vegades abans de posar-ho dins d'un bucle:
// Si horesObertes fos costós, això seria un desastre invisible
for (const tasca of tauler.tasques) {
console.log(tasca.horesEstimades / tauler.horesObertes); // ← recalcula a cada volta
}Amb sis tasques és igual. Amb cinc mil, aquell bucle passa a ser quadràtic i ningú no sospitaria d'una simple lectura de propietat. La regla és honesta: si el càlcul és car, dona-li forma de mètode (calcularHoresObertes()), perquè els parèntesis avisen que allà hi passa alguna cosa. I si necessites un getter car, guarda el resultat a la memòria cau internament i invalida'l quan l'estat canviï —una tècnica que és la memoïtzació de 03-06 aplicada a objectes, i que s'estudia amb calma al Mòdul 9.
- Camps privats
#camp
#campEls getters i setters controlen l'accés, però a l'apartat 3 el magatzem real continuava sent _hores: una propietat pública amb un nom que demana que no la toquis. Des d'ES2022 el llenguatge ofereix privacitat de debò.
'use strict';
class Tasca {
#estat = 'pendent'; // camp privat, amb valor inicial (R5)
constructor(dades) {
this.id = dades.id;
this.titol = dades.titol;
if (dades.estat !== undefined) this.#validarIAssignarEstat(dades.estat, true);
}
get estat() {
return this.#estat; // lectura pública, escriptura impossible
}
canviarEstat(nou) {
this.#validarIAssignarEstat(nou, false);
return this;
}
#validarIAssignarEstat(nou, esInicial) { /* apartat 6 */ }
}
const t = new Tasca({ id: 6, titol: 'Pressupost de la fusteria' });
console.log(t.estat); // 'pendent' ← es llegeix pel getter
// t.#estat; // ✗ SyntaxError: Private field '#estat' must be declared in an enclosing class
// t.estat = 'feta'; // ✗ TypeError: només té getterEls camps privats tenen unes propietats molt concretes que convé enumerar:
- El coixinet forma part del nom.
#estatiestatsón propietats diferents que poden coexistir, que és justament el que fem:#estatguarda,estat(getter) exposa. - S'han de declarar al cos de la classe. No en pots crear un al vol amb
this.#nou = 1si no està declarat: és unSyntaxError, detectat abans d'executar. - Només són accessibles des de dins de la classe que els declara. Ni tan sols des d'una subclasse: si
TascaRecurrent extends Tascanecessita l'estat, ha de fer servir el getter públic. - L'error és sintàctic, no d'execució. Accedir-hi des de fora ni tan sols compila, cosa que significa que la fallada apareix immediatament i no en producció.
- No apareixen a
Object.keys,for...in,JSON.stringify,Object.entriesni al spread{...obj}. Això últim té conseqüències que resoldrem a l'apartat 11.
Aquesta és la privacitat real, i és el que faltava per complir la promesa del 05-02:
const tasca = new Tasca({ id: 6, titol: 'Pressupost de la fusteria' });
tasca.estat = 'feta'; // en mode estricte → TypeError
console.log(tasca.estat); // 'pendent' ← intacta
tasca.canviarEstat('en-curs'); // ✓ únic camí
console.log(tasca.estat); // 'en-curs'
- Mètodes privats i estàtics privats
El coixinet també funciona en mètodes, camps estàtics i mètodes estàtics. Serveix per al mateix propòsit: separar el mecanisme intern de la interfície.
Aquesta és la versió completa de la Tasca encapsulada del projecte, l'exemple central de la lliçó:
'use strict';
class Tasca {
// ── Configuració privada de la classe ───────────────────────────
static #TRANSICIONS = { // R6
pendent: ['en-curs'],
'en-curs': ['pendent', 'feta'],
feta: []
};
static #MARQUES = { pendent: '○', 'en-curs': '▸', feta: '✓' };
static #PESOS = { alta: 3, mitjana: 2, baixa: 1 };
static #ultimId = 6; // R1: el backlog arriba fins al 6
// ── Estat privat de la instància ────────────────────────────────
#estat = 'pendent'; // R5
#hores;
#etiquetes = [];
constructor(dades) {
this.id = dades.id ?? Tasca.seguentId();
this.titol = Tasca.#validarTitol(dades.titol);
this.responsable = dades.responsable || null; // R8
this.prioritat = dades.prioritat ?? 'mitjana';
this.dataLimit = dades.dataLimit;
this.revisor = dades.revisor ?? null;
this.horesEstimades = dades.horesEstimades; // passa pel setter (R3)
this.etiquetes = dades.etiquetes ?? []; // passa pel setter (R9)
if (dades.estat !== undefined) this.#assignarEstatInicial(dades.estat);
}
// ── Estàtics ────────────────────────────────────────────────────
static seguentId() {
Tasca.#ultimId += 1;
return Tasca.#ultimId;
}
static #validarTitol(titol) { // mètode estàtic PRIVAT
if (typeof titol !== 'string' || titol.trim() === '') {
throw new ErrorDeValidacio('El títol no pot estar buit.', 'titol', titol); // R2
}
return titol.trim();
}
// ── Getters ─────────────────────────────────────────────────────
get estat() { return this.#estat; }
get horesEstimades() { return this.#hores; }
get etiquetes() { return [...this.#etiquetes]; } // còpia defensiva
get oberta() { return this.#estat !== 'feta'; }
get esforc() { return (Tasca.#PESOS[this.prioritat] ?? 0) * this.#hores; }
get marca() { return Tasca.#MARQUES[this.#estat]; }
// ── Setters ─────────────────────────────────────────────────────
set horesEstimades(valor) {
if (typeof valor !== 'number' || !(valor > 0 && valor <= 40)) {
throw new ErrorDeValidacio('Les hores han de ser un nombre entre 1 i 40.', 'horesEstimades', valor);
}
this.#hores = valor;
}
set etiquetes(llista) {
if (!Array.isArray(llista)) {
throw new ErrorDeValidacio('Les etiquetes han de ser un array.', 'etiquetes', llista);
}
this.#etiquetes = [...new Set(llista.map((e) => String(e).trim().toLowerCase()))].filter(Boolean); // R9
}
// ── Mètodes privats ─────────────────────────────────────────────
#assignarEstatInicial(estat) {
if (!Object.hasOwn(Tasca.#TRANSICIONS, estat)) {
throw new ErrorDeValidacio(`Estat desconegut: "${estat}".`, 'estat', estat);
}
this.#estat = estat;
}
#potAnarA(nou) {
return (Tasca.#TRANSICIONS[this.#estat] ?? []).includes(nou);
}
// ── Interfície pública ──────────────────────────────────────────
canviarEstat(nou) {
if (!Object.hasOwn(Tasca.#TRANSICIONS, nou)) {
throw new ErrorDeValidacio(`Estat desconegut: "${nou}".`, 'estat', nou);
}
if (!this.#potAnarA(nou)) {
throw new ErrorDeValidacio(
`Transició no permesa: "${this.#estat}" → "${nou}".`, 'estat', nou); // R6
}
this.#estat = nou;
return this;
}
estaVencuda(avui) {
return this.dataLimit < avui && this.oberta; // R10
}
descripcio(avui) {
const avis = avui !== undefined && this.estaVencuda(avui) ? ' ⚠ VENÇUDA' : '';
return `${this.marca} [${this.id}] ${this.titol} · ${this.responsable ?? 'sense assignar'} · ${this.#hores} h${avis}`;
}
}I la comprovació que l'objecte ja no es pot corrompre:
const fusteria = new Tasca({
id: 6, titol: ' Pressupost de la fusteria ', responsable: 'Iván',
prioritat: 'alta', etiquetes: ['Fusteria', 'compres', 'FUSTERIA', ' '],
horesEstimades: 5, dataLimit: '2026-09-05', revisor: 'Marta'
});
console.log(fusteria.titol); // 'Pressupost de la fusteria' ← trim, R2
console.log(fusteria.etiquetes); // [ 'fusteria', 'compres' ] ← R9
console.log(fusteria.descripcio('2026-09-20')); // ○ [6] Pressupost de la fusteria · Iván · 5 h ⚠ VENÇUDA
console.log(fusteria.esforc); // 15
// Els quatre atacs de l'apartat 1, ara:
try { fusteria.estat = 'feta'; } catch (e) { console.log('✓ bloquejat:', e.constructor.name); }
try { fusteria.horesEstimades = 500; } catch (e) { console.log('✓ bloquejat:', e.message); }
try { fusteria.canviarEstat('arxivada'); } catch (e) { console.log('✓ bloquejat:', e.message); }
try { fusteria.canviarEstat('feta'); } catch (e) { console.log('✓ bloquejat:', e.message); }
console.log(fusteria.estat, fusteria.horesEstimades); // 'pendent' 5 ← intacteVal la pena assenyalar dues decisions de disseny d'aquell codi:
La còpia defensiva del getter etiquetes. Retornar [...this.#etiquetes] en lloc de l'array intern impedeix que algú faci tasca.etiquetes.push('URGENT') i se salti la R9 mutant des de fora. És exactament el perill de la còpia superficial que vas estudiar a 04-08, aplicat aquí com a defensa. El preu és que crea un array nou a cada lectura, així que es justifica només quan l'array és petit i el risc és real.
Els setters es fan servir també des del constructor. this.horesEstimades = dades.horesEstimades dins del constructor invoca el setter i aplica la R3. Si el constructor escrivís this.#hores = dades.horesEstimades, la validació existiria només per a les modificacions posteriors, i la regla acabaria duplicada. Una regla, un lloc.
- L'operador
#camp in objecte
#camp in objecteCom que accedir a un camp privat des de fora és un SyntaxError, no se'n pot comprovar l'existència amb un try/catch. Per a això existeix una forma especial de l'operador in, anomenada brand check («comprovació de marca»):
'use strict';
class Tasca {
#estat = 'pendent';
/** És aquest objecte una Tasca autèntica, creada per aquesta classe? */
static esTasca(objecte) {
return #estat in objecte;
}
}
const real = new Tasca({ id: 1, titol: 'Redissenyar la sala polivalent', horesEstimades: 12, dataLimit: '2026-09-30' });
const impostor = { id: 1, titol: 'Redissenyar la sala polivalent', estat: 'pendent' };
console.log(Tasca.esTasca(real)); // true
console.log(Tasca.esTasca(impostor)); // false
console.log(Tasca.esTasca(null)); // false (in retorna false, no llança, per a objectes)L'expressió #estat in objecte només es pot escriure dins de la classe que declara #estat, i retorna true o false sense llançar mai. Per què no fer servir instanceof, que ja coneixes de 05-01? Perquè instanceof mira la cadena de prototips, i aquella es pot falsificar:
const falsificat = Object.create(Tasca.prototype);
console.log(falsificat instanceof Tasca); // true ← menteix!
console.log(Tasca.esTasca(falsificat)); // false ← la marca no es pot falsificar
// falsificat.descripcio(); // ✗ TypeError: no té els camps privatsAquell objecte té els mètodes però cap dels camps privats, perquè mai no va passar pel constructor. Un instanceof el donaria per bo i el programa rebentaria més endavant, en un lloc confús. El brand check és la comprovació honesta: només els objectes que realment van passar pel constructor d'aquesta classe tenen els seus camps privats. És la tècnica adequada quan un mètode necessita garantir que pot tocar l'estat intern del seu argument.
- Les alternatives històriques:
_camp, closures i WeakMap
_camp, closures i WeakMapEls camps # van arribar el 2022. Abans es feien servir tres tècniques, i les trobaràs en qualsevol codi amb uns quants anys, així que convé reconèixer-les.
La convenció del guió baix. Un _ davant del nom significa «això és intern, no ho toquis». No hi ha cap mecanisme al darrere: és un acord entre persones.
class TascaAntiga {
constructor(estat) { this._estat = estat; }
get estat() { return this._estat; }
}
const t = new TascaAntiga('pendent');
t._estat = 'arxivada'; // funciona perfectament: res no ho impedeixClosures (el patró mòdul de 03-04). L'estat viu en variables capturades per les funcions retornades. És privacitat real, la mateixa de crearMagatzemTasques.
function crearTasca(dadesInicials) {
let estat = 'pendent'; // ← inaccessible des de fora, de debò
const transicions = { pendent: ['en-curs'], 'en-curs': ['pendent', 'feta'], feta: [] };
return {
id: dadesInicials.id,
titol: dadesInicials.titol,
get estat() { return estat; },
canviarEstat(nou) {
if (!transicions[estat].includes(nou)) throw new ErrorDeValidacio('Transició no permesa.', 'estat', nou);
estat = nou;
return this;
}
};
}Funciona, però té el defecte que va motivar tot el mòdul: els mètodes es creen de nou a cada crida, perquè viuen a l'objecte retornat i no en un prototip. És el problema de l'apartat 1 de 05-01.
WeakMap amb la instància com a clau. El truc que feien servir les llibreries serioses abans d'ES2022: una taula externa que associa cada instància amb les seves dades privades.
const privats = new WeakMap();
class TascaWeak {
constructor(dades) {
privats.set(this, { estat: 'pendent' });
this.id = dades.id;
}
get estat() { return privats.get(this).estat; }
canviarEstat(nou) { privats.get(this).estat = nou; return this; }
}És privacitat real i compatible amb prototips, però verbosa. El detall de la Weak: manté referències febles, de manera que quan la instància deixa de fer-se servir el recol·lector d'escombraries se'n pot endur també la seva entrada. Un Map normal aquí provocaria una fuita de memòria, tema del Mòdul 9.
La comparativa completa:
| Tècnica | Privacitat real? | Mètodes al prototip | Verbositat | Quan la veuràs |
|---|---|---|---|---|
_camp (convenció) |
No | Sí | Mínima | Codi anterior al 2022, i en equips que prefereixen no bloquejar les proves |
| Closure (patró mòdul) | Sí | No | Mitjana | Funcions fàbrica, mòduls de configuració |
WeakMap extern |
Sí | Sí | Alta | Llibreries anteriors a ES2022 |
#camp |
Sí | Sí | Mínima | L'opció per defecte avui |
Amb una excepció honesta sobre #: com que és privacitat real, les teves proves tampoc no poden mirar-hi dins (Mòdul 8). Això obliga a provar l'objecte a través de la seva interfície pública, que és el correcte, però exigeix dissenyar bé aquella interfície. Alguns equips continuen preferint _ per aquesta raó; és una decisió d'equip defensable, no un error.
- Propietats de només lectura i
Object.defineProperty
Object.definePropertyQueda un forat obert al model: l'id. És una propietat pública normal, i la R1 diu que l'assigna l'aplicació i ningú no el canvia.
Un getter sense setter sobre un camp privat ja ho resoldria, però hi ha una eina més precisa i de propòsit general: Object.defineProperty, que permet crear una propietat especificant-ne els atributs en lloc de només el valor.
'use strict';
class Tasca {
constructor(dades) {
Object.defineProperty(this, 'id', {
value: dades.id,
writable: false, // no es pot reassignar
enumerable: true, // sí que apareix a Object.keys i JSON.stringify
configurable: false // no es pot esborrar ni redefinir
});
this.titol = dades.titol;
}
}
const t = new Tasca({ id: 6, titol: 'Pressupost de la fusteria' });
console.log(t.id); // 6
// t.id = 999; // ✗ TypeError: Cannot assign to read only property 'id'
// delete t.id; // ✗ TypeError: Cannot delete property 'id'
console.log(Object.keys(t)); // [ 'id', 'titol' ]
console.log(JSON.stringify(t)); // {"id":6,"titol":"Pressupost de la fusteria"}Els tres atributs, amb el seu efecte pràctic:
| Atribut | Si és true |
Si és false |
Per defecte amb defineProperty |
|---|---|---|---|
writable |
Es pot reassignar amb = |
Assignar llança TypeError en mode estricte (i s'ignora sense ell) |
false |
enumerable |
Apareix a Object.keys, for...in, spread, JSON.stringify |
Existeix però es manté invisible a aquestes operacions | false |
configurable |
Es pot esborrar amb delete o redefinir |
delete i redefinir llancen TypeError |
false |
Parany clàssic: una propietat creada amb assignació normal (
this.id = 6) té els tres atributs atrue; una creada ambObject.definePropertyels té afalsesi no els indiques. Si defineixes una propietat i després «desapareix» deJSON.stringify, és que et vas oblidar d'enumerable: true.
Pots inspeccionar els atributs de qualsevol propietat:
console.log(Object.getOwnPropertyDescriptor(t, 'id'));
// { value: 6, writable: false, enumerable: true, configurable: false }
console.log(Object.getOwnPropertyDescriptor(t, 'titol'));
// { value: '…', writable: true, enumerable: true, configurable: true }I això explica per fi dues coses que van quedar soltes en lliçons anteriors. La primera, per què els mètodes d'Object.prototype no sortien a for...in a 05-01: són no enumerables. La segona, com fer que un mètode sigui invisible a l'enumeració sense fer servir class:
Una comparació amb Object.freeze, que vas conèixer a 04-08:
| Eina | Abast | Ús típic |
|---|---|---|
writable: false en una propietat |
Una propietat | L'id d'una tasca (R1) |
Object.freeze(obj) |
Totes les propietats de l'objecte, superficialment | Les constants PESOS, MARQUES |
#camp privat |
Invisible des de fora, escrivible des de dins | L'#estat, que sí que canvia per mètodes |
Les tres es combinen segons el que vulguis: invisible (privat), immutable (writable: false) o totes dues coses.
- Dissenyar l'API pública de
Tauler
TaulerAmb totes les peces, l'exercici de disseny més valuós: decidir què es veu i què no a la peça central del projecte. La pregunta que guia la decisió és sempre la mateixa: què necessita de debò qui fa servir aquest objecte?
A la classe Tauler del 05-02, this.tasques era públic. Això significa que qualsevol pot fer tauler.tasques.push(cosaQualsevol), saltant-se la validació d'afegir, la R1 d'identificadors únics i la R7 de càrrega màxima. La llista de tasques és precisament l'invariant que el tauler ha de protegir.
'use strict';
class Tauler {
#tasques = [];
#nom;
constructor(nom, tasques = []) {
this.#nom = nom;
for (const tasca of tasques) this.afegir(tasca); // tota entrada passa per la porta oficial
}
// ── Lectures ────────────────────────────────────────────────────
get nom() { return this.#nom; }
get total() { return this.#tasques.length; }
get tasques() { return [...this.#tasques]; } // còpia: mutar-la no afecta el tauler
get obertes() { return this.#tasques.filter((t) => t.oberta); }
get horesTotals() { return this.#tasques.reduce((s, t) => s + t.horesEstimades, 0); }
get horesObertes() { return this.obertes.reduce((s, t) => s + t.horesEstimades, 0); }
get esforc() { return this.#tasques.reduce((s, t) => s + t.esforc, 0); }
cercarPerId(id) { return this.#tasques.find((t) => t.id === id) ?? null; }
filtrar(predicat) { return this.#tasques.filter(predicat); }
vencudes(avui) { return this.obertes.filter((t) => t.estaVencuda(avui)); }
horesPerResponsable() {
return this.obertes.reduce((acc, t) => {
const clau = t.responsable ?? 'sense assignar';
acc[clau] = (acc[clau] ?? 0) + t.horesEstimades;
return acc;
}, {});
}
resum(avui) {
return {
total: this.total, obertes: this.obertes.length,
horesTotals: this.horesTotals, horesObertes: this.horesObertes,
vencudes: this.vencudes(avui).length, esforc: this.esforc
};
}
// ── Escriptures (les úniques portes) ────────────────────────────
afegir(tasca) {
if (!(tasca instanceof Tasca)) throw new ErrorDeValidacio("Només s'admeten instàncies de Tasca.", 'tasca', tasca);
if (this.cercarPerId(tasca.id)) throw new ErrorDeValidacio(`Id duplicat: ${tasca.id}.`, 'id', tasca.id); // R1
this.#comprovarCarrega(tasca); // R7
this.#tasques.push(tasca);
return this;
}
eliminar(id) {
const i = this.#tasques.findIndex((t) => t.id === id);
if (i === -1) throw new ErrorDeValidacio(`No existeix la tasca ${id}.`, 'id', id);
return this.#tasques.splice(i, 1)[0];
}
canviarEstat(id, nou) {
const tasca = this.cercarPerId(id);
if (tasca === null) throw new ErrorDeValidacio(`No existeix la tasca ${id}.`, 'id', id);
tasca.canviarEstat(nou);
return this;
}
// ── Mecanisme intern ────────────────────────────────────────────
#comprovarCarrega(tasca) {
if (!tasca.oberta || tasca.responsable === null) return;
const actual = this.horesPerResponsable()[tasca.responsable] ?? 0;
if (actual + tasca.horesEstimades > 40) {
throw new ErrorDeValidacio(
`${tasca.responsable} arribaria a ${actual + tasca.horesEstimades} h (màxim 40).`, 'horesEstimades', tasca.horesEstimades);
}
}
}En ús, amb el backlog canònic:
const tauler = new Tauler('Taller Nómada', dadesBacklog.map((d) => new Tasca(d)));
console.log(tauler.resum('2026-09-20'));
// { total: 6, obertes: 5, horesTotals: 48, horesObertes: 45, vencudes: 1, esforc: 124 }
console.log(tauler.horesPerResponsable()); // { 'Iván': 25, 'Marta': 6, 'Lucía': 14 }
// Els intents de rodejar l'API:
tauler.tasques.push('això no és una tasca');
console.log(tauler.total); // 6 ← es va mutar una còpia, el tauler no se'n va assabentar
try { tauler.afegir('això tampoc'); }
catch (e) { console.log('✓ bloquejat:', e.message); } // Només s'admeten instàncies de Tasca.El criteri de disseny, resumit en una taula:
| S'exposa | No s'exposa | Motiu |
|---|---|---|
nom, total, resum() |
L'array #tasques real |
És l'invariant que cal protegir |
tasques com a còpia |
— | Permet recórrer sense permetre mutar |
afegir, eliminar, canviarEstat |
#comprovarCarrega |
És mecanisme, no interfície |
filtrar(predicat) |
— | Dona flexibilitat total sense obrir l'array |
Dos matisos honestos. El getter tasques copia l'array, però les tasques de dins són les mateixes instàncies: és una còpia superficial (04-08), així que qui la rebi sí que pot cridar tasca.canviarEstat(...). Això és intencionat —la tasca ja protegeix el seu propi estat—, però convé saber-ho. I filtrar(predicat) és una porta deliberadament oberta: en lloc d'escriure vint mètodes de consulta, es deixa que el consumidor porti el seu propi predicat, que és exactament la funció d'ordre superior de 03-06.
- El que és privat i
JSON.stringify: el rescat de toJSON
JSON.stringify: el rescat de toJSONHi ha un preu a pagar per tota aquesta protecció, i apareix així que intentes desar les dades.
const tasca = new Tasca({
id: 6, titol: 'Pressupost de la fusteria', responsable: 'Iván',
prioritat: 'alta', etiquetes: ['fusteria', 'compres'],
horesEstimades: 5, dataLimit: '2026-09-05', revisor: 'Marta'
});
console.log(JSON.stringify(tasca));
// {"id":6,"titol":"Pressupost de la fusteria","responsable":"Iván","prioritat":"alta","dataLimit":"2026-09-05","revisor":"Marta"}Falten estat, horesEstimades i etiquetes. El motiu el saps des de 04-08: JSON.stringify recorre les propietats pròpies enumerables, i els camps privats no ho són. Els getters tampoc no compten: viuen al prototip, i stringify no mira la cadena. El resultat és un JSON silenciosament incomplet, que és el pitjor tipus de fallada possible: no llança cap error i només es descobreix en importar les dades.
La solució és el mètode toJSON que vas conèixer a 04-08: si un objecte el té, JSON.stringify fa servir el seu valor de retorn en lloc de l'objecte.
class Tasca {
// …tot l'anterior…
/** Representació serialitzable: inclou el que és privat com a camps públics. */
toJSON() {
return {
id: this.id,
titol: this.titol,
responsable: this.responsable,
prioritat: this.prioritat,
estat: this.#estat,
etiquetes: [...this.#etiquetes],
horesEstimades: this.#hores,
dataLimit: this.dataLimit,
revisor: this.revisor
};
}
/** Camí invers: reconstrueix una instància a partir de dades planes. */
static desDeJSON(dades) {
return new Tasca(typeof dades === 'string' ? JSON.parse(dades) : dades);
}
}
console.log(JSON.stringify(tasca));
// {"id":6,…,"estat":"pendent","etiquetes":["fusteria","compres"],"horesEstimades":5,…}I al tauler, el cicle complet d'exportació i importació de 04-08, ara amb classes:
class Tauler {
// …tot l'anterior…
toJSON() {
return { versio: 1, nom: this.#nom, tasques: this.#tasques }; // stringify crida el toJSON de cada tasca
}
static importar(text) {
let dades;
try {
dades = JSON.parse(text);
} catch (error) {
throw new ErrorDeDades('El backlog no és JSON vàlid.', error);
}
if (dades.versio !== 1) throw new ErrorDeDades(`Versió no admesa: ${dades.versio}.`);
return new Tauler(dades.nom, dades.tasques.map((d) => Tasca.desDeJSON(d)));
}
}
const text = JSON.stringify(tauler);
const recuperat = Tauler.importar(text);
console.log(recuperat.resum('2026-09-20'));
// { total: 6, obertes: 5, horesTotals: 48, horesObertes: 45, vencudes: 1, esforc: 124 }
console.log(recuperat.cercarPerId(6) instanceof Tasca); // true ← instàncies de debòEl parell toJSON / desDeJSON és el patró que has de recordar: una sortida i una entrada explícites, controlades per la classe, que a més revaliden en importar. Sense ell, tot el que és privat es perd en desar.
Un avís final sobre
structuredClone, que vas estudiar a 04-08: no funciona amb camps privats. Llança unDataCloneErrorperquè no pot reconstruir la classe. La còpia profunda d'una instància es fa ambTasca.desDeJSON(tasca.toJSON()), o amb un mètodeclonar()que faci exactament això.
Errors Habituals i Consells
- Recursió infinita al setter.
set estat(v) { this.estat = v; }es crida a si mateix fins a esgotar la pila (elRangeErrorde 03-05). El magatzem intern ha de tenir un altre nom:#estat. - Validar només al setter i saltar-se'l al constructor. Si el constructor escriu
this.#hores = dades.horesen lloc dethis.horesEstimades = dades.hores, la validació no s'aplica en la creació. Assigna sempre a través de la propietat pública. - Fer servir
#campdes d'una subclasse. Els privats no s'hereten:TascaRecurrentno pot llegirthis.#estat. Si una subclasse necessita accés, exposa un getter (oprotectedper convenció amb_, que JavaScript no té com a mecanisme). - Oblidar
enumerable: trueaObject.defineProperty. Per defecte ésfalse, i la propietat desapareix d'Object.keysi deJSON.stringifysense avisar. - Getters cars. Un getter que ordena o recorre milers d'elements enganya qui el llegeix. Si costa, que sigui un mètode amb parèntesis.
- Getters amb efectes secundaris. Un getter que modifica l'objecte, incrementa un comptador o llança errors en condicions normals trenca l'expectativa que llegir una propietat és innocu, i fa la depuració molt difícil (la mateixa consola del navegador executa els getters en inspeccionar un objecte).
- Retornar l'array intern des d'un getter.
get etiquetes() { return this.#etiquetes; }deixa la porta oberta atasca.etiquetes.push(…). Retorna una còpia si l'array ha de mantenir invariants. - Encapsular per encapsular. Un objecte de dades pures —una fila llegida d'una API, un punt
{x, y}— no hi guanya res amb quinze getters trivials. L'encapsulació és per a objectes que tenen regles a protegir. - Consell: per decidir què fer privat, escriu primer com vols que es faci servir l'objecte des de fora. Tot el que no aparegui en aquelles línies d'ús és candidat a ser privat.
Exercicis
Exercici 1 — Resident amb quota d'hores. Escriu una classe Resident per al coworking de Taller Nómada amb:
- camps privats
#horesReservades(inicial 0) i#pla; nomde només lectura mitjançantObject.defineProperty(R1 aplicada a persones);- getters
pla,horesReservades,horesDisponiblesialDia(truesi li queden hores); - un setter
plaque només accepti'mensual'(40 h) o'diari'(8 h) i que rebutgi un canvi de pla si les hores ja reservades superarien la quota del pla nou; - un mètode
reservar(hores)que validi i sumi, llançantErrorDeValidaciosi excedeix la quota.
Exercici 2 — De _ a #. Converteix aquesta classe heretada a camps privats, getters i setters, i localitza la fallada d'encapsulació que el guió baix deixava passar.
class Pressupost {
constructor(limit) {
this._limit = limit;
this._linies = [];
}
get total() { return this._linies.reduce((s, l) => s + l.quantitat, 0); }
get linies() { return this._linies; }
afegir(concepte, quantitat) {
if (this.total + quantitat > this._limit) throw new Error('Es supera el límit.');
this._linies.push({ concepte, quantitat });
}
}Exercici 3 — Serialització amb privats. Partint de la classe Tasca de l'apartat 6, afegeix toJSON(), static desDeJSON(dades) i un mètode clonar(). Demostra amb codi que: (a) sense toJSON es perden tres camps; (b) amb toJSON el viatge d'anada i tornada ho conserva tot; (c) el clon és independent de l'original —canviar l'estat d'un no afecta l'altre— i (d) structuredClone falla.
Solucions
Exercici 1
'use strict';
class Resident {
static #QUOTES = { mensual: 40, diari: 8 };
#pla;
#horesReservades = 0;
constructor(nom, pla) {
Object.defineProperty(this, 'nom', { value: nom, writable: false, enumerable: true, configurable: false });
this.pla = pla; // passa pel setter: valida
}
get pla() { return this.#pla; }
get horesReservades() { return this.#horesReservades; }
get quota() { return Resident.#QUOTES[this.#pla]; }
get horesDisponibles() { return this.quota - this.#horesReservades; }
get alDia() { return this.horesDisponibles > 0; }
set pla(nou) {
if (!Object.hasOwn(Resident.#QUOTES, nou)) {
throw new ErrorDeValidacio(`Pla desconegut: "${nou}".`, 'pla', nou);
}
if (this.#horesReservades > Resident.#QUOTES[nou]) {
throw new ErrorDeValidacio(
`No es pot passar a "${nou}": ja hi ha ${this.#horesReservades} h reservades i la quota és ${Resident.#QUOTES[nou]}.`,
'pla', nou);
}
this.#pla = nou;
}
reservar(hores) {
if (typeof hores !== 'number' || hores <= 0) {
throw new ErrorDeValidacio('Les hores han de ser un nombre positiu.', 'hores', hores);
}
if (this.#horesReservades + hores > this.quota) {
throw new ErrorDeValidacio(
`${this.nom} només té ${this.horesDisponibles} h disponibles.`, 'hores', hores);
}
this.#horesReservades += hores;
return this;
}
}
const lucia = new Resident('Lucía', 'mensual');
lucia.reservar(14).reservar(12);
console.log(lucia.horesReservades, lucia.horesDisponibles, lucia.alDia); // 26 14 true
try { lucia.pla = 'diari'; }
catch (e) { console.error(e.message); } // No es pot passar a "diari": ja hi ha 26 h reservades i la quota és 8.
try { lucia.nom = 'Lucia'; }
catch (e) { console.error(e.constructor.name); } // TypeError
try { lucia.reservar(20); }
catch (e) { console.error(e.message); } // Lucía només té 14 h disponibles.El detall interessant és el setter de pla: no valida només el valor entrant, sinó la coherència amb l'estat actual. Aquesta és la mena d'invariant que només es pot protegir des de dins de l'objecte, perquè només ell coneix les dues peces de la dada. I observa que el constructor assigna this.pla = pla en lloc de this.#pla = pla: una regla, un lloc.
Exercici 2
La fallada era al getter linies: retornava l'array intern, així que qualsevol podia saltar-se completament la comprovació del límit.
const p = new Pressupost(500);
p.afegir('Tinta serigràfica', 120);
p.linies.push({ concepte: 'Torn nou', quantitat: 9000 }); // ✗ ningú no comprova res
console.log(p.total); // 9120, amb límit 500Versió corregida:
'use strict';
class Pressupost {
#limit;
#linies = [];
constructor(limit) {
if (typeof limit !== 'number' || limit <= 0) {
throw new ErrorDeValidacio('El límit ha de ser un nombre positiu.', 'limit', limit);
}
this.#limit = limit;
}
get limit() { return this.#limit; }
get total() { return this.#linies.reduce((s, l) => s + l.quantitat, 0); }
get disponible() { return this.#limit - this.total; }
get linies() { return this.#linies.map((l) => ({ ...l })); } // còpia de 2 nivells
afegir(concepte, quantitat) {
if (this.total + quantitat > this.#limit) {
throw new ErrorDeValidacio(
`Es supera el límit: queden ${this.disponible} € i se'n demanen ${quantitat} €.`, 'quantitat', quantitat);
}
this.#linies.push({ concepte, quantitat });
return this;
}
}
const p = new Pressupost(500);
p.afegir('Tinta serigràfica', 120).afegir('Paper per enquadernar', 80);
p.linies.push({ concepte: 'Torn nou', quantitat: 9000 }); // muta una còpia
console.log(p.total, p.disponible); // 200 300 ← el pressupost no se'n va assabentar
try { p.afegir('Torn nou', 9000); }
catch (e) { console.error(e.message); } // Es supera el límit: queden 300 € i se'n demanen 9000 €.Fixa't en get linies(): no n'hi ha prou amb [...this.#linies], perquè això copiaria l'array però compartiria els objectes de dins —la còpia superficial de 04-08—, i algú podria fer p.linies[0].quantitat = 9000. Per això es fa servir map((l) => ({ ...l })), que copia també cada línia. És la profunditat justa: un nivell més perquè les línies són objectes plans.
Exercici 3
'use strict';
class Tasca {
// …camps privats i mètodes de l'apartat 6…
toJSON() {
return {
id: this.id, titol: this.titol, responsable: this.responsable,
prioritat: this.prioritat, estat: this.#estat,
etiquetes: [...this.#etiquetes], horesEstimades: this.#hores,
dataLimit: this.dataLimit, revisor: this.revisor
};
}
static desDeJSON(dades) {
return new Tasca(typeof dades === 'string' ? JSON.parse(dades) : dades);
}
clonar() {
return Tasca.desDeJSON(this.toJSON());
}
}
const original = new Tasca({
id: 1, titol: 'Redissenyar la sala polivalent', responsable: 'Iván',
prioritat: 'alta', estat: 'en-curs', etiquetes: ['espai', 'disseny'],
horesEstimades: 12, dataLimit: '2026-09-30', revisor: 'Marta'
});
// (a) Sense toJSON es perdrien tres camps. Simulant-ho amb el spread:
console.log(Object.keys({ ...original }));
// [ 'id', 'titol', 'responsable', 'prioritat', 'dataLimit', 'revisor' ]
// falten: estat, etiquetes, horesEstimades
// (b) Amb toJSON, anada i tornada complet
const text = JSON.stringify(original);
const tornada = Tasca.desDeJSON(text);
console.log(tornada.estat, tornada.horesEstimades, tornada.etiquetes);
// 'en-curs' 12 [ 'espai', 'disseny' ]
console.log(tornada instanceof Tasca); // true
// (c) El clon és independent
const copia = original.clonar();
copia.canviarEstat('feta');
console.log(original.estat, copia.estat); // 'en-curs' 'feta'
copia.etiquetes = ['espai'];
console.log(original.etiquetes); // [ 'espai', 'disseny' ] ← intactes
// (d) structuredClone no pot amb els privats
try { structuredClone(original); }
catch (e) { console.error(e.name); } // 'DataCloneError'Tres conclusions d'aquest exercici. La primera, que el spread {...tasca} de 04-07 no veu el que és privat, exactament igual que JSON.stringify: la raó és la mateixa, tots dos operen sobre propietats pròpies enumerables. La segona, que clonar() implementat com a desDeJSON(toJSON()) és una còpia profunda que a més revalida: si l'original tingués dades corruptes, el clon fallaria en construir-se, cosa que és un avantatge, no un inconvenient. I la tercera, que structuredClone deixa de ser una opció així que hi ha classes amb privats, cosa que col·loca el parell toJSON/desDeJSON com el mecanisme de còpia i persistència oficial del projecte.
Conclusió
Has convertit una recomanació en una barrera. En començar la lliçó, tasca.estat = 'feta' i tasca.horesEstimades = 500 travessaven tota la validació del 05-02 sense resistència; ara una llança TypeError perquè la propietat només té getter, i l'altra llança ErrorDeValidacio des del setter. L'estat real viu a #estat, #hores i #etiquetes, camps que ni tan sols es poden anomenar des de fora de la classe —l'error és sintàctic, no d'execució—, i les regles R2, R3, R5, R6, R8 i R9 estan escrites cadascuna en un sol lloc, en el camí pel qual obligatòriament passa qualsevol valor.
Tens les quatre eines i saps quan fer servir cadascuna. get per a propietats calculades que mai no es dessincronitzen (oberta, diesRestants, horesObertes, esforc), amb el criteri de reservar-lo per al que és barat i sense arguments i deixar els mètodes per a les accions i els càlculs cars. set per interceptar les assignacions i validar abans de guardar, recordant sempre que el magatzem intern s'ha de dir diferent per no caure en la recursió infinita, i que el constructor ha d'assignar a través del setter per no duplicar la regla. #camp, #metode() i static #camp per a la privacitat real, amb el brand check #estat in objecte com a comprovació d'autenticitat que instanceof no pot oferir. I Object.defineProperty amb writable, enumerable i configurable per a les propietats de només lectura com l'id, sabent que els tres atributs valen false per defecte i que aquí hi ha l'origen de la meitat de les sorpreses.
Saps a més d'on ve tot això: la convenció _camp que no protegia res, els closures del patró mòdul de 03-04 que sí que protegien però renunciaven al prototip, i el WeakMap extern de les llibreries anteriors al 2022. Has dissenyat l'API pública de Tauler decidint conscientment què es veu —resum, horesPerResponsable, filtrar, afegir, canviarEstat— i què s'amaga —l'array #tasques i el mètode #comprovarCarrega—, amb còpies defensives als getters perquè ni tan sols un push sobre el que es retorna pugui trencar els invariants. I has pagat i resolt el preu de la privacitat: com que els camps # són invisibles a JSON.stringify, al spread i a structuredClone, el parell toJSON / desDeJSON es converteix en la porta oficial d'entrada i sortida de dades, amb revalidació inclosa i amb el resum canònic intacte després del viatge: 48 h, 45 obertes, 1 vençuda, esforç 124.
El model de Nómada Tasques ja és complet i protegit. Però continua vivint sencer en un sol fitxer: Tasca, TascaRecurrent, Tauler, ErrorDeValidacio, les constants, el backlog d'exemple i el codi que l'executa, tot barrejat i creixent sense fre. Amb <script> solts, a més, cada nom que declares competeix amb tots els altres al mateix espai global, i l'ordre de càrrega es converteix en una dependència implícita que ningú no ha escrit enlloc. Ordenar això —separar el projecte en fitxers amb fronteres explícites, que declaren què exposen i què necessiten— és el tema de Mòduls i Importació/Exportació.
Curs de JavaScript: De Principiant a Avançat
Mòdul 1: Introducció a JavaScript
- 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
