El Mòdul 4 va acabar amb un problema molt concret: cada tasca de Nómada Tasques ha de saber descriure's, comprovar si està vençuda i canviar d'estat, però escriure aquests mètodes dins de cada objecte significa que sis-centes tasques arrossegarien sis-centes còpies del mateix codi. Cal un motlle compartit. A gairebé tots els llenguatges aquest motlle es diu «classe» i funciona per còpia d'una plantilla. A JavaScript el mecanisme és diferent i més simple del que sembla: cada objecte guarda un enllaç a un altre objecte, i quan li demanes una propietat que no té, la busca allà. Aquest enllaç s'anomena prototip, i és el cor del llenguatge. En aquesta lliçó l'entendràs del tot: com es resol l'accés a una propietat pas a pas, què fa exactament new —tancant el deute que vam deixar anotat a 04-02—, com es construeix una jerarquia d'herència a mà i per què les classes que veuràs a la lliçó següent no són res més que una façana elegant sobre tot això.
Contingut
- El problema de repetir mètodes objecte a objecte
- Cada objecte té un enllaç
[[Prototype]] - La cadena de prototips: com es resol l'accés a una propietat
- Llegir i escriure el prototip:
getPrototypeOf,setPrototypeOf,__proto__ Object.create: crear un objecte amb el prototip que tu diguis- La propietat
prototypede les funcions - Què fa
newexactament: els quatre passos - El constructor
Tascai els seus mètodes aTasca.prototype - Per què això estalvia memòria
instanceof,constructoriisPrototypeOf- Propietats pròpies davant d'heretades
- Herència entre constructors «a mà»
Object.prototypei per què tot objecte tétoString- No toquis els prototips natius
- Errors Habituals i Consells
- Exercicis
- Conclusió
- El problema de repetir mètodes objecte a objecte
Comencem posant el problema al davant, amb codi que ja saps escriure. Aquesta és una manera directa de donar comportament a una tasca: una funció fàbrica que retorna l'objecte amb els seus mètodes a dins.
'use strict';
const AVUI = '2026-09-20';
const MARQUES = { pendent: '○', 'en-curs': '▸', feta: '✓' };
function crearTascaAmbMetodes(id, titol, responsable, horesEstimades, dataLimit, estat) {
return {
id,
titol,
responsable,
horesEstimades,
dataLimit,
estat,
// Els mètodes es creen DE NOU a cada crida a la fàbrica
estaVencuda(avui) {
return this.dataLimit < avui && this.estat !== 'feta';
},
descripcio() {
return `${MARQUES[this.estat]} [${this.id}] ${this.titol} · ${this.responsable} · ${this.horesEstimades} h`;
}
};
}
const t1 = crearTascaAmbMetodes(1, 'Redissenyar la sala polivalent', 'Iván', 12, '2026-09-30', 'en-curs');
const t6 = crearTascaAmbMetodes(6, 'Pressupost de la fusteria', 'Iván', 5, '2026-09-05', 'pendent');
console.log(t1.descripcio()); // ▸ [1] Redissenyar la sala polivalent · Iván · 12 h
console.log(t6.estaVencuda(AVUI)); // trueFunciona perfectament. I tanmateix té un defecte que es veu en una sola línia:
descripcio no és la mateixa funció a les dues tasques. Cada crida a la fàbrica ha creat un objecte funció nou, amb el seu propi espai a la memòria, per fer exactament el mateix. Amb dues tasques és igual. Amb les sis-centes de què parlava el tancament del mòdul anterior, i amb quatre o cinc mètodes per tasca, són milers de funcions idèntiques ocupant memòria sense aportar res.
El que volem és això:
I per aconseguir-ho cal entendre el mecanisme de cerca de propietats de JavaScript.
- Cada objecte té un enllaç
[[Prototype]]
[[Prototype]]La regla fonamental, i d'ella es deriva tota la resta:
Tot objecte de JavaScript té una referència interna a un altre objecte (o a
null), anomenada[[Prototype]]. Quan demanes una propietat que l'objecte no té, el motor la busca en aquest altre objecte. I si tampoc no hi és, al prototip d'aquell, i així successivament.
Els claudàtors dobles de [[Prototype]] són la notació que fa servir l'especificació del llenguatge per a les ranures internes: no és una propietat que puguis escriure amb un punt, sinó un enllaç que el motor manté per dins. Sí que existeixen maneres oficials de consultar-lo i modificar-lo, que veuràs a l'apartat 4.
Un exemple mínim, sense constructors ni res complicat:
'use strict';
// Un objecte que farà de "motlle": conté comportament compartit
const comportamentDeTasca = {
descripcio() {
return `[${this.id}] ${this.titol}`;
}
};
// Un objecte el [[Prototype]] del qual és l'anterior
const tasca = Object.create(comportamentDeTasca);
tasca.id = 1;
tasca.titol = 'Redissenyar la sala polivalent';
console.log(tasca.descripcio()); // '[1] Redissenyar la sala polivalent'
console.log(Object.hasOwn(tasca, 'descripcio')); // false ← no la té ellFixa't en el que acaba de passar: tasca no té cap propietat descripcio, i tot i així tasca.descripcio() funciona. El motor no l'ha trobada a tasca, ha seguit l'enllaç fins a comportamentDeTasca, l'ha trobada allà i l'ha executada.
I hi ha un detall crucial, que enllaça directament amb el que vas aprendre a 04-02: encara que la funció visqui al prototip, this continua sent l'objecte sobre el qual s'ha fet la crida. Aquesta és exactament la regla de les quatre formes d'invocació: this és el que hi ha a l'esquerra del punt, no on es va escriure la funció. Per això this.id val 1.
const altra = Object.create(comportamentDeTasca);
altra.id = 6;
altra.titol = 'Pressupost de la fusteria';
console.log(altra.descripcio()); // '[6] Pressupost de la fusteria'
console.log(tasca.descripcio === altra.descripcio); // true ← la mateixa funció!Ja tenim l'objectiu de l'apartat 1 acomplert: una sola funció compartida per totes les tasques.
- La cadena de prototips: com es resol l'accés a una propietat
Quan escrius objecte.propietat, el motor segueix aquest algorisme:
- Té
objecteuna propietat pròpia anomenadapropietat? Si sí, en retorna el valor i acaba. - Si no, pren el
[[Prototype]]d'objecte. Ésnull? Aleshores retornaundefinedi acaba. - Si no és
null, repeteix el pas 1 sobre aquell objecte.
Aquesta seqüència d'objectes enllaçats és la cadena de prototips. És una llista, no un arbre: cada objecte té exactament un prototip, i la cadena sempre acaba en null.
flowchart TD
T["tasca<br/>{ id: 1, titol: '…' }"] -->|"[[Prototype]]"| P["comportamentDeTasca<br/>{ descripcio() }"]
P -->|"[[Prototype]]"| O["Object.prototype<br/>{ toString, hasOwnProperty, valueOf … }"]
O -->|"[[Prototype]]"| N["null<br/>(final de la cadena)"]
Seguim tres accessos diferents sobre aquest diagrama:
| Accés | Recorregut | Resultat |
|---|---|---|
tasca.titol |
Trobat al primer objecte | 'Redissenyar la sala polivalent' |
tasca.descripcio |
No és a tasca → trobat a comportamentDeTasca |
la funció |
tasca.toString |
No és a tasca → ni a comportamentDeTasca → trobat a Object.prototype |
la funció nativa |
tasca.responsable |
No és enlloc; la cadena acaba en null |
undefined |
I ara la part que gairebé ningú no explica: llegir i escriure no són simètrics. La cadena només es recorre en llegir. En assignar, JavaScript crea (o modifica) una propietat pròpia a l'objecte de l'esquerra, sense tocar el prototip.
'use strict';
const motlle = { prioritat: 'mitjana' };
const a = Object.create(motlle);
const b = Object.create(motlle);
console.log(a.prioritat, b.prioritat); // 'mitjana' 'mitjana' ← totes dues hereten
a.prioritat = 'alta'; // NO modifica el motlle
console.log(a.prioritat); // 'alta' ← propietat pròpia nova
console.log(b.prioritat); // 'mitjana' ← intacta
console.log(motlle.prioritat); // 'mitjana' ← intacte
console.log(Object.hasOwn(a, 'prioritat')); // true
console.log(Object.hasOwn(b, 'prioritat')); // falseAixò s'anomena ombrejat (shadowing): la propietat pròpia d'a tapa l'heretada, igual que a 03-04 una variable local tapava una de fora. És exactament el comportament que volem: cada tasca té les seves dades pròpies i comparteix el comportament del prototip.
Compte amb l'excepció: si la propietat heretada és un objecte o un array i el mutes en lloc de reassignar-lo, sí que estàs tocant el compartit.
a.etiquetes.push('x')no crea res nou: llegeixetiquetesde la cadena i modifica aquell array, que veuen tots. Per això els prototips han de contenir mètodes, no dades mutables.
- Llegir i escriure el prototip:
getPrototypeOf, setPrototypeOf, __proto__
getPrototypeOf, setPrototypeOf, __proto__Hi ha tres maneres de tocar el [[Prototype]] d'un objecte ja creat.
'use strict';
const motlle = { salutacio: 'Hola' };
const obj = Object.create(motlle);
// LLEGIR: forma correcta
console.log(Object.getPrototypeOf(obj) === motlle); // true
// ESCRIURE: forma correcta, però desaconsellada (vegeu més avall)
const altreMotlle = { salutacio: 'Bones' };
Object.setPrototypeOf(obj, altreMotlle);
console.log(obj.salutacio); // 'Bones'
// LLEGAT: __proto__ fa les dues coses
console.log(obj.__proto__ === altreMotlle); // true
obj.__proto__ = motlle; // equival a setPrototypeOf
console.log(obj.salutacio); // 'Hola'| Forma | Què fa | Cal fer-la servir? |
|---|---|---|
Object.getPrototypeOf(obj) |
Retorna el prototip | Sí, és la forma estàndard de llegir-lo |
Object.setPrototypeOf(obj, p) |
Canvia el prototip d'un objecte existent | Només si no hi ha alternativa: és molt lent |
obj.__proto__ |
Llegeix o escriu el prototip | No en codi nou: és llegat, existeix només per compatibilitat |
Object.create(p) |
Crea un objecte nou ja amb prototip p |
Sí, és la forma recomanada |
Sobre el rendiment de setPrototypeOf: els motors de JavaScript optimitzen molt l'accés a propietats suposant que la «forma» d'un objecte no canvia. Canviar el prototip sobre la marxa invalida totes aquestes optimitzacions per a aquell objecte i per al codi que el fa servir. La regla pràctica és: decideix el prototip en el moment de crear l'objecte i no el canviïs després.
Object.create: crear un objecte amb el prototip que tu diguis
Object.create: crear un objecte amb el prototip que tu diguisObject.create(proto) crea un objecte buit el [[Prototype]] del qual és proto. Admet a més un segon argument amb descriptors de propietat (els veuràs amb detall a 05-03).
const ambPrototip = Object.create({ a: 1 });
console.log(ambPrototip.a); // 1
const sensePrototip = Object.create(null);
console.log(Object.getPrototypeOf(sensePrototip)); // null
console.log(sensePrototip.toString); // undefined ← no hereta resAquest Object.create(null) té un ús molt concret que ja et vas trobar a 04-01: els diccionaris. Quan indexaves el backlog perId, les claus venien de dades; si alguna dada es digués 'toString' o 'constructor', un objecte literal normal et donaria una sorpresa, perquè aquestes propietats ja existeixen heretades.
const normal = {};
console.log('toString' in normal); // true ← heretada, encara que l'objecte estigui buit
console.log(normal.constructor); // [Function: Object]
const diccionari = Object.create(null);
console.log('toString' in diccionari); // false ← net de debòPer a un mapa de clau→valor amb claus impredictibles, Object.create(null) (o directament un Map, com vas veure a 04-05) evita tota aquesta mena de col·lisions.
- La propietat
prototype de les funcions
prototype de les funcionsAquí arriba la part que més confusió genera de tot el tema, i la causa és una desafortunada superposició de noms. Hi ha dues coses diferents que es diuen gairebé igual:
| Nom | Qui el té | Què és |
|---|---|---|
[[Prototype]] (es llegeix amb Object.getPrototypeOf) |
Tots els objectes | L'enllaç que se segueix en buscar una propietat |
.prototype |
Només les funcions (les normals, no les fletxa) | Un objecte normal que es farà servir com a [[Prototype]] de les instàncies que creï aquella funció amb new |
Dit en una frase: Tasca.prototype no és el prototip de Tasca; és el prototip que tindran els objectes creats amb new Tasca(...).
'use strict';
function Tasca(id, titol) {
this.id = id;
this.titol = titol;
}
console.log(typeof Tasca.prototype); // 'object'
console.log(Tasca.prototype.constructor === Tasca); // true
const t = new Tasca(1, 'Redissenyar la sala polivalent');
console.log(Object.getPrototypeOf(t) === Tasca.prototype); // true ← aquesta és la clau!
console.log(Object.getPrototypeOf(Tasca) === Function.prototype); // true (Tasca és una funció)Quan declares una funció normal, JavaScript li crea automàticament un objecte prototype buit, amb una única propietat no enumerable: constructor, que apunta de tornada a la funció. Les funcions fletxa no tenen prototype i per això no es poden fer servir amb new; és una altra conseqüència del que vas veure a 03-02 i 04-02.
const fletxa = () => {};
console.log(fletxa.prototype); // undefined
// new fletxa(); // ✗ TypeError: fletxa is not a constructor
- Què fa
new exactament: els quatre passos
new exactament: els quatre passosAra sí, tanquem el deute de 04-02. Quan escrius new Tasca(1, 'Redissenyar…'), el motor fa quatre coses en aquest ordre:
- Crea un objecte buit.
- Enllaça el seu
[[Prototype]]aTasca.prototype. - Executa el cos de
Tascaambthisapuntant a aquest objecte nou, passant-li els arguments. - Retorna aquest objecte, tret que el cos retorni explícitament un altre objecte (si retorna un primitiu o res, s'ignora i es retorna l'objecte nou).
flowchart TD
A["new Tasca(1, 'Redissenyar…')"] --> B["1 · const obj = {}"]
B --> C["2 · Object.setPrototypeOf(obj, Tasca.prototype)"]
C --> D["3 · Tasca.call(obj, 1, 'Redissenyar…')<br/>el cos assigna this.id, this.titol…"]
D --> E{"El cos ha retornat<br/>un objecte?"}
E -->|"Sí"| F["Es retorna AQUELL objecte"]
E -->|"No"| G["Es retorna obj"]
Pots comprovar-ho escrivint el teu propi new amb eines que ja coneixes: l'Object.create de l'apartat 5 i l'apply de 04-02.
'use strict';
/** Reimplementació didàctica de l'operador new. */
function elMeuNew(Constructor, ...parametres) {
const obj = Object.create(Constructor.prototype); // passos 1 i 2
const resultat = Constructor.apply(obj, parametres); // pas 3
return typeof resultat === 'object' && resultat !== null ? resultat : obj; // pas 4
}
function Tasca(id, titol) {
this.id = id;
this.titol = titol;
this.estat = 'pendent'; // R5: tota tasca neix pendent
}
Tasca.prototype.descripcio = function () {
return `[${this.id}] ${this.titol} (${this.estat})`;
};
const ambNew = new Tasca(7, 'Revisar la guillotina');
const ambElMeuNew = elMeuNew(Tasca, 7, 'Revisar la guillotina');
console.log(ambNew.descripcio()); // [7] Revisar la guillotina (pendent)
console.log(ambElMeuNew.descripcio()); // [7] Revisar la guillotina (pendent)
console.log(ambElMeuNew instanceof Tasca); // trueQue les dues versions donin el mateix demostra que new no és màgia: és sucre sobre Object.create + una crida amb this fixat.
Per convenció, les funcions pensades per fer-se servir amb new s'escriuen amb inicial majúscula (Tasca, Tauler, ErrorDeValidacio). No és una regla del llenguatge, és un senyal per a qui llegeix el codi. I oblidar el new en mode estricte dona un error immediat, cosa que és una sort:
'use strict';
// const trencada = Tasca(8, 'Sense new');
// ✗ TypeError: Cannot set properties of undefined (setting 'id')
// perquè this és undefined en una crida simple
- El constructor
Tasca i els seus mètodes a Tasca.prototype
Tasca i els seus mètodes a Tasca.prototypeAmb totes les peces, aquest és el model de Nómada Tasques escrit amb constructor i prototip. És la versió «clàssica» del que a la lliçó següent escriuràs amb class.
'use strict';
const AVUI = '2026-09-20';
const PESOS = { alta: 3, mitjana: 2, baixa: 1 };
const MARQUES = { pendent: '○', 'en-curs': '▸', feta: '✓' };
/**
* Constructor de tasques de Nómada Tasques.
* @param {Object} dades camps del model canònic
*/
function Tasca(dades) {
this.id = dades.id;
this.titol = dades.titol;
this.responsable = dades.responsable ?? null; // R8: mai una cadena buida
this.prioritat = dades.prioritat ?? 'mitjana';
this.estat = dades.estat ?? 'pendent'; // R5
this.etiquetes = dades.etiquetes ?? [];
this.horesEstimades = dades.horesEstimades;
this.dataLimit = dades.dataLimit;
this.revisor = dades.revisor ?? null;
}
// ── Comportament compartit: viu UNA sola vegada, al prototip ──
Tasca.prototype.estaVencuda = function (avui) {
return this.dataLimit < avui && this.estat !== 'feta'; // R10
};
Tasca.prototype.esforc = function () {
return (PESOS[this.prioritat] ?? 0) * this.horesEstimades;
};
Tasca.prototype.descripcio = function () {
const marca = MARQUES[this.estat] ?? '?';
return `${marca} [${this.id}] ${this.titol} · ${this.responsable ?? 'sense assignar'} · ${this.horesEstimades} h`;
};
const tasca1 = 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'
});
const tasca6 = new Tasca({
id: 6, titol: 'Pressupost de la fusteria', responsable: 'Iván',
prioritat: 'alta', estat: 'pendent', etiquetes: ['fusteria', 'compres'],
horesEstimades: 5, dataLimit: '2026-09-05', revisor: 'Marta'
});
console.log(tasca1.descripcio()); // ▸ [1] Redissenyar la sala polivalent · Iván · 12 h
console.log(tasca6.estaVencuda(AVUI)); // true ← la fusteria, la vençuda de sempre
console.log(tasca1.esforc()); // 36 (3 × 12)
console.log(tasca1.descripcio === tasca6.descripcio); // true ← objectiu acomplertAmb el backlog complet, els números canònics continuen sortint:
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 backlog = dadesBacklog.map((d) => new Tasca(d));
const obertes = backlog.filter((t) => t.estat !== 'feta');
console.log(backlog.length); // 6
console.log(backlog.reduce((s, t) => s + t.horesEstimades, 0)); // 48
console.log(obertes.reduce((s, t) => s + t.horesEstimades, 0)); // 45
console.log(obertes.filter((t) => t.estaVencuda(AVUI)).length); // 1
console.log(backlog.reduce((s, t) => s + t.esforc(), 0)); // 124Els mateixos 48, 45, 1 i 124 de sempre —però ara cada element de l'array és una Tasca, amb comportament propi, i map, filter i reduce de 04-05 continuen funcionant exactament igual.
- Per què això estalvia memòria
La diferència entre posar els mètodes dins del constructor o al prototip és fàcil de veure amb un diagrama.
flowchart TD
subgraph Malament["Mètodes dins del constructor"]
A1["tasca1<br/>dades + estaVencuda + esforc + descripcio"]
A2["tasca2<br/>dades + estaVencuda + esforc + descripcio"]
A3["tasca3<br/>dades + estaVencuda + esforc + descripcio"]
end
subgraph Be["Mètodes al prototip"]
B1["tasca1<br/>només dades"] --> BP["Tasca.prototype<br/>estaVencuda · esforc · descripcio"]
B2["tasca2<br/>només dades"] --> BP
B3["tasca3<br/>només dades"] --> BP
end
Amb N tasques i M mètodes:
| Estratègia | Objectes funció creats | Amb N = 600, M = 3 |
|---|---|---|
Mètodes com a propietats pròpies (this.descripcio = function…) |
N × M | 1 800 funcions |
| Mètodes al prototip | M | 3 funcions |
I hi ha un segon benefici, menys citat però igual d'important: si corregeixes un mètode, la correcció arriba a totes les instàncies ja creades, perquè totes llegeixen del mateix objecte.
Tasca.prototype.descripcio = function () {
return `${MARQUES[this.estat]} #${this.id} ${this.titol}`; // format nou
};
console.log(tasca1.descripcio()); // ▸ #1 Redissenyar la sala polivalent
console.log(tasca6.descripcio()); // ○ #6 Pressupost de la fusteriaCap de les dues tasques no s'ha creat de nou: simplement resolen descripcio a la cadena i troben la versió actual. (Aquest poder també és perillós; l'apartat 14 explica on és el límit.)
El cost, per ser honestos: llegir una propietat heretada obliga a recórrer una baula més de la cadena. És una diferència irrellevant a la pràctica —els motors guarden el resultat a la memòria cau— però explica per què cadenes de deu nivells no són bona idea.
instanceof, constructor i isPrototypeOf
instanceof, constructor i isPrototypeOfJa vas fer servir instanceof a 04-08 sense explicació. Ara pots entendre què fa exactament: obj instanceof F recorre la cadena de prototips d'obj buscant l'objecte F.prototype.
console.log(tasca1 instanceof Tasca); // true ← Tasca.prototype és a la seva cadena
console.log(tasca1 instanceof Object); // true ← Object.prototype també
console.log([] instanceof Array); // true
console.log([] instanceof Object); // true
console.log(tasca1 instanceof Array); // falseEls altres dos mecanismes relacionats:
// constructor: la propietat que el prototip porta de sèrie
console.log(tasca1.constructor === Tasca); // true
console.log(tasca1.constructor.name); // 'Tasca'
// isPrototypeOf: pregunta a l'inrevés, des del prototip
console.log(Tasca.prototype.isPrototypeOf(tasca1)); // true
console.log(Object.prototype.isPrototypeOf(tasca1)); // true| Eina | Pregunta que respon | Advertiment |
|---|---|---|
obj instanceof F |
És F.prototype a la cadena d'obj? |
És el que es fa servir en el 99 % dels casos |
obj.constructor |
Quina funció figura com a constructora? | És una propietat normal i sobreescrivible: no és fiable com a comprovació de seguretat |
P.isPrototypeOf(obj) |
És P una baula de la cadena d'obj? |
Útil quan tens el prototip però no el constructor (objectes d'Object.create) |
L'avís sobre constructor no és teòric. Si reemplaces l'objecte prototype sencer en lloc d'afegir-li propietats, perds aquella referència:
function Nota(text) { this.text = text; }
Nota.prototype = { // ✗ s'ha reemplaçat l'objecte sencer
llegir() { return this.text; }
};
const n = new Nota('hola');
console.log(n.llegir()); // 'hola' (funciona)
console.log(n.constructor === Nota); // false ← s'ha perdut
console.log(n.constructor === Object);// true ← ara apunta al literal
// Solució si fas això: restaurar-lo a mà
Nota.prototype.constructor = Nota;La forma segura és afegir mètodes un a un (Nota.prototype.llegir = …), com a l'apartat 8.
- Propietats pròpies davant d'heretades
Aquesta distinció, que a 04-01 vas introduir amb Object.hasOwn, ara pren tot el sentit.
console.log(Object.hasOwn(tasca1, 'titol')); // true ← dada pròpia
console.log(Object.hasOwn(tasca1, 'descripcio')); // false ← heretat del prototip
console.log('descripcio' in tasca1); // true ← in SÍ que mira la cadenaLa diferència entre in i Object.hasOwn és precisament si es recorre o no la cadena de prototips:
| Expressió | Mira l'objecte? | Mira la cadena? |
|---|---|---|
Object.hasOwn(obj, 'x') |
Sí | No |
obj.hasOwnProperty('x') |
Sí | No (però el mètode sí que s'hereta; Object.hasOwn és la versió moderna i segura) |
'x' in obj |
Sí | Sí |
I ara s'explica el comportament de for...in que a 04-01 se't va presentar com un advertiment: for...in recorre les propietats enumerables pròpies i heretades.
'use strict';
function Punt(x) { this.x = x; }
Punt.prototype.descriure = function () { return `x=${this.x}`; };
const p = new Punt(5);
for (const clau in p) console.log(clau);
// x
// descriure ← el mètode heretat també!
console.log(Object.keys(p)); // [ 'x' ] ← només les pròpiesPer què, doncs, for...in sobre les tasques de l'apartat 8 no imprimia descripcio? Perquè sí que ho faria. És exactament la raó per la qual a 04-01 la recomanació va ser fer servir Object.keys/values/entries, que només consideren les propietats pròpies. Si per algun motiu necessites for...in, la protecció clàssica és filtrar:
Un apunt per completar el quadre: les propietats que porta Object.prototype (toString, valueOf, hasOwnProperty…) no apareixen a for...in perquè estan marcades com a no enumerables. Els mètodes que tu afegeixes a un prototip amb una assignació normal sí que són enumerables, i per això sí que apareixen. A 05-03 veuràs com controlar-ho amb Object.defineProperty, i a la lliçó següent descobriràs que els mètodes de class són no enumerables per defecte —una de les moltes millores discretes que aporten.
- Herència entre constructors «a mà»
Taller Nómada té tasques que es repeteixen cada setmana: recollir el material de serigrafia, revisar les reserves. Una tasca recurrent és una tasca amb tot el d'una tasca normal, més una periodicitat. És el cas de llibre de l'herència: volem que TascaRecurrent tingui tot el comportament de Tasca i hi afegeixi el seu.
Amb constructors, l'herència es munta en dos passos que cal fer explícitament.
'use strict';
// ── Pas 1: heretar les DADES ──────────────────────────────────────
function TascaRecurrent(dades) {
Tasca.call(this, dades); // executa el constructor pare sobre AQUEST objecte
this.periodicitat = dades.periodicitat ?? 'setmanal';
}
// ── Pas 2: heretar el COMPORTAMENT ────────────────────────────────
TascaRecurrent.prototype = Object.create(Tasca.prototype);
TascaRecurrent.prototype.constructor = TascaRecurrent; // restaurar la referència
// ── Mètodes propis del fill ───────────────────────────────────────
TascaRecurrent.prototype.properaData = function () {
const dies = { setmanal: 7, quinzenal: 14, mensual: 30 };
const base = new Date(this.dataLimit);
base.setDate(base.getDate() + (dies[this.periodicitat] ?? 7));
return base.toISOString().slice(0, 10);
};
// ── Sobreescriptura: el fill redefineix un mètode del pare ────────
TascaRecurrent.prototype.descripcio = function () {
const base = Tasca.prototype.descripcio.call(this); // crida la versió del pare
return `${base} · es repeteix ${this.periodicitat}`;
};
const neteja = new TascaRecurrent({
id: 8, titol: 'Recollir el material de serigrafia', responsable: 'Marta',
prioritat: 'baixa', horesEstimades: 1, dataLimit: '2026-09-25',
periodicitat: 'setmanal'
});
console.log(neteja.descripcio());
// ○ [8] Recollir el material de serigrafia · Marta · 1 h · es repeteix setmanal
console.log(neteja.properaData()); // '2026-10-02'
console.log(neteja.estaVencuda(AVUI)); // false ← mètode heretat de l'avi
console.log(neteja instanceof TascaRecurrent); // true
console.log(neteja instanceof Tasca); // trueEls dos passos mereixen que els miris per separat, perquè són la font de gairebé tots els errors d'aquest tema:
Tasca.call(this, dades)és elcallde 04-02 posat a treballar. Executa el cos del constructor pare ambthisapuntant a l'objecte que s'està construint, de manera que totes les assignacions (this.id = …,this.titol = …) hi cauen a dins. Sense aquesta línia, la tasca recurrent tindria mètodes però cap dada.Object.create(Tasca.prototype)crea un objecte buit enllaçat al prototip del pare, i el posa com aprototypedel fill. Un error freqüentíssim és escriureTascaRecurrent.prototype = Tasca.prototype(senseObject.create): aleshores tots dos comparteixen el mateix objecte, i qualsevol mètode que afegeixis al fill apareix també al pare.
Així queda la cadena completa:
flowchart TD
L["neteja<br/>{ id: 8, titol, periodicitat… }"] -->|"[[Prototype]]"| TR["TascaRecurrent.prototype<br/>{ properaData, descripcio, constructor }"]
TR -->|"[[Prototype]]"| TP["Tasca.prototype<br/>{ estaVencuda, esforc, descripcio }"]
TP -->|"[[Prototype]]"| OP["Object.prototype<br/>{ toString, hasOwnProperty… }"]
OP -->|"[[Prototype]]"| N["null"]
I amb aquest diagrama s'explica la sobreescriptura: quan demanes neteja.descripcio, el motor troba la versió de TascaRecurrent.prototype abans d'arribar a la de Tasca.prototype, així que fa servir la del fill. La del pare continua allà, tapada, i per això el fill pot invocar-la explícitament amb Tasca.prototype.descripcio.call(this).
Retén aquesta cerimònia de cinc línies —call, Object.create, restaurar constructor, .call per al mètode del pare—, perquè a la lliçó següent es redueix a dues paraules: extends i super.
Object.prototype i per què tot objecte té toString
Object.prototype i per què tot objecte té toStringAl final de gairebé tota cadena de prototips hi ha Object.prototype. D'allà vénen mètodes que has fet servir sense preguntar-te d'on sortien:
const tasca = { id: 1, titol: 'Redissenyar la sala polivalent' };
console.log(tasca.toString()); // '[object Object]'
console.log(tasca.hasOwnProperty('id')); // true
console.log(tasca.valueOf() === tasca); // true
console.log(Object.getPrototypeOf(tasca) === Object.prototype); // trueAquell '[object Object]' que apareix quan concatenes un objecte amb un string —i que a 01-07 et vam avisar que era un mal símptoma— és literalment el toString heretat d'Object.prototype. Pots donar-li una implementació millor a les teves tasques:
Tasca.prototype.toString = function () {
return `Tasca#${this.id} «${this.titol}»`;
};
console.log(`Registrada: ${tasca1}`); // 'Registrada: Tasca#1 «Redissenyar la sala polivalent»'
console.log(tasca1 + ''); // 'Tasca#1 «Redissenyar la sala polivalent»'La conversió implícita a string de 01-07 consulta aquest mètode, així que ara els teus objectes s'imprimeixen de manera útil. Fixa't que això és diferent de toJSON (04-08), que només intervé a JSON.stringify.
Cada tipus integrat té el seu propi prototip intermedi, tots ells enllaçats a Object.prototype:
| Valor | La seva cadena |
|---|---|
[1, 2, 3] |
Array.prototype → Object.prototype → null |
'hola' (en accedir a un mètode) |
String.prototype → Object.prototype → null |
function f(){} |
Function.prototype → Object.prototype → null |
new Map() |
Map.prototype → Object.prototype → null |
Object.create(null) |
null (sense cadena) |
Això respon a una pregunta que potser et vas fer al Mòdul 4: per què un array té map, filter i reduce. No els té l'array; els té Array.prototype, i tots els arrays els troben seguint el seu enllaç. El mateix amb 'text'.toUpperCase(): els strings són primitius, però en cridar un mètode el motor els embolcalla momentàniament en un objecte String que sí que té la cadena.
- No toquis els prototips natius
Com que acabes de veure que els mètodes es comparteixen per la cadena, la temptació és immediata: «si afegeixo un mètode a Array.prototype, tots els arrays del programa el tindran».
// ✗✗✗ NO HO FACIS
Array.prototype.ultim = function () {
return this[this.length - 1];
};
console.log([1, 2, 3].ultim()); // 3 (funciona… fins que deixa de funcionar)Modificar un prototip natiu s'anomena monkey patching, i és una de les poques coses que la comunitat considera mala pràctica gairebé sense excepcions. Els motius:
- Col·lisions. Si una llibreria afegeix
Array.prototype.ultimamb un altre comportament, una de les dues guanya i l'altra falla en silenci. - Col·lisions amb el futur del llenguatge. Ha passat de debò: codi que afegia
Array.prototype.flattenva trencar pàgines senceres quan l'estàndard va incorporarflat. El comitè va haver de canviar el nom per compatibilitat. - Contamina
for...in. Com vas veure a l'apartat 11, un mètode afegit així és enumerable i apareixerà en qualsevolfor...insobre un array de qualsevol part del programa. - Sorprèn qui llegeix el codi. Un
.ultim()que no és a la documentació de JavaScript obliga a buscar on dimonis es defineix.
Les alternatives són sempre millors:
// ✓ Una funció normal
function ultim(array) {
return array[array.length - 1];
}
// ✓ O directament el que ja porta el llenguatge (04-03)
console.log([1, 2, 3].at(-1)); // 3La regla completa: afegeix mètodes als prototips que crees tu (Tasca.prototype), mai als que no són teus (Array.prototype, Object.prototype, String.prototype…).
Errors Habituals i Consells
- Confondre
prototypeamb[[Prototype]].Tasca.prototypeés una propietat de la funció i només interessa a l'hora de crear instàncies;Object.getPrototypeOf(tasca)és l'enllaç real de l'objecte. La relació entre tots dos és una sola línia:Object.getPrototypeOf(new Tasca()) === Tasca.prototype. - Oblidar
new. En mode estricte obtens unTypeErrorimmediat perquèthisésundefined; sense mode estricte, les assignacions cauen a l'objecte global i ho contaminen tot en silenci. Una raó més per al'use strict'que arrossegues des de 01-04. Les classes de 05-02 fan impossible aquest error. - Posar dades mutables al prototip.
Tasca.prototype.etiquetes = []sembla inofensiu, però totes les tasques compartirien aquell mateix array, i unpushdes d'una el canviaria per a totes. Les dades van al constructor (propietats pròpies); al prototip, només mètodes i constants immutables. - Reemplaçar l'objecte
prototypesencer (Tasca.prototype = { … }) trenca la propietatconstructori, si ja havies creat instàncies, aquestes continuen enllaçades a l'objecte antic. Afegeix mètodes un a un, o restauraconstructorexplícitament. - Oblidar
Pare.call(this, …)en heretar: la instància filla té els mètodes però cap dada, i tot surtundefined. És l'error número u de l'herència manual. - Escriure
Fill.prototype = Pare.prototypeen lloc d'Object.create(Pare.prototype): comparteixen objecte, i els mètodes del fill es filtren al pare. - Fer servir
Object.setPrototypeOfen calent. És correcte però lent; decideix el prototip en crear l'objecte. - Refiar-se d'
obj.constructorper decidir lògica. És una propietat normal que qualsevol pot trepitjar. Per comprovar el tipus,instanceof. - Consell de depuració: a la consola del navegador, expandir un objecte mostra les seves propietats pròpies i, al final, una entrada
[[Prototype]]que pots desplegar per veure la cadena sencera. És la manera més ràpida de comprovar si l'herència ha quedat ben muntada.
Exercicis
Exercici 1 — La cadena, sobre el paper. Donat aquest codi, indica sense executar-lo què imprimeix cada console.log i en quina baula de la cadena es resol cada propietat.
const base = { tipus: 'tasca', descriure() { return `Sóc una ${this.tipus}`; } };
const filla = Object.create(base);
filla.tipus = 'subtasca';
const neta = Object.create(filla);
console.log(neta.tipus); // (a)
console.log(neta.descriure()); // (b)
console.log(Object.hasOwn(neta, 'tipus')); // (c)
neta.tipus = 'microtasca';
console.log(filla.tipus, base.tipus); // (d)
console.log(Object.keys(neta)); // (e)Exercici 2 — Constructor Subtasca que hereta de Tasca. Partint del constructor Tasca de l'apartat 8, escriu Subtasca(dades) que:
- hereti dades i comportament de
Tasca; - afegeixi una propietat
pareId; - sobreescrigui
descripcio()perquè el resultat vagi sagnat amb dos espais i acabi en← subtasca de #pareId; - afegeixi un mètode
esFulla()que retornitruesithis.subtasquesés buit o no existeix.
Comprova-ho amb la subtasca 13 «Pintar i muntar» (5 h, pendent, de la tasca 1), i verifica els dos instanceof.
Exercici 3 — comptarMetodesPropis. Escriu una funció inventari(obj) que retorni un objecte { propies, heretades, cadena } on propies sigui el nombre de propietats pròpies enumerables, heretades el nombre de propietats enumerables que només apareixen a la cadena, i cadena un array amb els noms dels constructors de cada baula (per exemple ['TascaRecurrent', 'Tasca', 'Object']). Prova-la amb la tasca recurrent de l'apartat 12.
Solucions
Exercici 1
(a) 'subtasca' → no és a neta; es troba a filla (pròpia de filla)
(b) 'Sóc una subtasca'
→ descriure es resol a base (dues baules amunt),
però this és neta, i this.tipus es resol a filla → 'subtasca'
(c) false → neta no té tipus propi (encara)
(d) 'subtasca' 'tasca'
→ l'assignació ha creat una propietat PRÒPIA a neta; la cadena no es toca
(e) [ 'tipus' ] → després de l'assignació, neta ja en té una de pròpiaEl punt (b) és el més instructiu: la funció viu a base, però this es decideix a la crida (neta.descriure()), així que apunta a neta; i des d'allà, this.tipus torna a recórrer la cadena fins a filla. Mètode i dades es resolen en baules diferents, i això és completament normal.
Exercici 2
'use strict';
function Subtasca(dades) {
Tasca.call(this, dades); // 1 · dades del pare
this.pareId = dades.pareId;
this.subtasques = dades.subtasques ?? [];
}
Subtasca.prototype = Object.create(Tasca.prototype); // 2 · comportament del pare
Subtasca.prototype.constructor = Subtasca; // 3 · restaurar constructor
Subtasca.prototype.descripcio = function () {
const base = Tasca.prototype.descripcio.call(this);
return ` ${base} ← subtasca de #${this.pareId}`;
};
Subtasca.prototype.esFulla = function () {
return !Array.isArray(this.subtasques) || this.subtasques.length === 0;
};
const pintar = new Subtasca({
id: 13, titol: 'Pintar i muntar', responsable: 'Iván',
prioritat: 'mitjana', horesEstimades: 5, dataLimit: '2026-09-28',
pareId: 1
});
console.log(pintar.descripcio());
// ○ [13] Pintar i muntar · Iván · 5 h ← subtasca de #1
console.log(pintar.esFulla()); // true
console.log(pintar.estaVencuda(AVUI)); // false ← heretat de Tasca.prototype
console.log(pintar.esforc()); // 10 (mitjana = 2 × 5 h)
console.log(pintar instanceof Subtasca); // true
console.log(pintar instanceof Tasca); // trueEls tres passos numerats són la recepta completa de l'herència manual. Si ometeixes el primer, pintar.titol seria undefined; si ometeixes el segon, pintar.estaVencuda no existiria; si ometeixes el tercer, tot funciona però pintar.constructor mentiria dient Tasca.
Exercici 3
'use strict';
function inventari(obj) {
const propies = Object.keys(obj);
const totes = [];
for (const clau in obj) totes.push(clau); // pròpies + heretades enumerables
const cadena = [];
let actual = Object.getPrototypeOf(obj);
while (actual !== null) {
cadena.push(actual.constructor?.name ?? '(sense constructor)');
actual = Object.getPrototypeOf(actual);
}
return {
propies: propies.length,
heretades: totes.length - propies.length,
cadena
};
}
console.log(inventari(neteja));
// { propies: 10, heretades: 5, cadena: [ 'TascaRecurrent', 'Tasca', 'Object' ] }Tres coses que aquest exercici deixa clares. Primer, Object.keys i for...in difereixen exactament en les propietats heretades enumerables: restant l'un de l'altre surten els comptes. Segon, el while que recorre la cadena amb Object.getPrototypeOf fins a arribar a null és el patró estàndard per inspeccionar una jerarquia, i la condició de parada (!== null) és el «cas base» de 03-07 aplicat a una iteració. I tercer, els cinc heretats són els mètodes que vam posar amb assignació normal als dos prototips: els d'Object.prototype no apareixen perquè són no enumerables.
Conclusió
Has arribat al mecanisme que sosté tot el sistema d'objectes de JavaScript, i resulta ser una sola idea repetida: cada objecte guarda un enllaç a un altre objecte, i les propietats que no troba en si mateix les busca allà. D'aquesta idea surten totes les conseqüències que has vist. La cadena de prototips es recorre en llegir, però mai en escriure, i per això una assignació crea sempre una propietat pròpia que ombreja l'heretada. Object.create(proto) és la manera neta de fabricar un objecte amb la cadena que vulguis —inclosa la cadena buida d'Object.create(null) per a diccionaris—, mentre que Object.getPrototypeOf la consulta, Object.setPrototypeOf la canvia a un cost alt i __proto__ és un llegat que no has d'escriure.
Has separat per fi les dues coses que es diuen semblant: [[Prototype]], que tenen tots els objectes, i .prototype, que només tenen les funcions i que serveix per a una cosa concreta: ser el prototip de les instàncies que aquella funció creï amb new. I has desmuntat new en els seus quatre passos —crear l'objecte, enllaçar-lo al prototype, executar el cos amb this apuntant-hi, retornar-lo—, fins al punt d'haver-lo reimplementat amb Object.create i apply. Aquella tercera fila de la taula d'invocacions de 04-02 ja no té res de misteriós.
Sobre aquesta base has construït el model real: function Tasca(dades) amb les nou propietats del model canònic com a dades pròpies, i estaVencuda, esforc i descripcio vivint una sola vegada a Tasca.prototype. Els números de sempre —48 h, 45 obertes, la fusteria vençuda, esforç 124— surten igual, però ara amb tres funcions a la memòria en lloc de mil vuit-centes. Saps distingir propietats pròpies d'heretades amb Object.hasOwn davant d'in, entens per fi per què for...in recorre les heretades i Object.keys no, i pots comprovar tipus amb instanceof, isPrototypeOf i —amb reserves— constructor. Has muntat una herència completa a mà amb Pare.call(this, …) i Object.create(Pare.prototype), sobreescrivint un mètode i cridant la versió del pare. I saps per què Array.prototype té map, per què tot objecte respon a toString, i per què mai no has d'afegir res a aquells prototips que no són teus.
Tota aquesta cerimònia funciona, però és sorollosa: tres línies de ritual per cada relació d'herència, un constructor que cal restaurar a mà, mètodes que es declaren lluny del constructor i que a més queden enumerables sense voler. Des del 2015 el llenguatge ofereix una sintaxi que fa exactament això mateix —sense canviar el mecanisme ni un pèl— però de manera llegible, segura i amb les correccions ja aplicades de sèrie. És el tema de Classes i Programació Orientada a Objectes, on reescriuràs Tasca, TascaRecurrent i un Tauler complet amb class, extends i super, i on entendràs per fi, del tot, aquell class ErrorDeValidacio extends Error que fas servir com a recepta des del Mòdul 2.
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
