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

  1. El problema de repetir mètodes objecte a objecte
  2. Cada objecte té un enllaç [[Prototype]]
  3. La cadena de prototips: com es resol l'accés a una propietat
  4. Llegir i escriure el prototip: getPrototypeOf, setPrototypeOf, __proto__
  5. Object.create: crear un objecte amb el prototip que tu diguis
  6. La propietat prototype de les funcions
  7. Què fa new exactament: els quatre passos
  8. El constructor Tasca i els seus mètodes a Tasca.prototype
  9. Per què això estalvia memòria
  10. instanceof, constructor i isPrototypeOf
  11. Propietats pròpies davant d'heretades
  12. Herència entre constructors «a mà»
  13. Object.prototype i per què tot objecte té toString
  14. No toquis els prototips natius
  15. Errors Habituals i Consells
  16. Exercicis
  17. Conclusió

  1. 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));   // true

Funciona perfectament. I tanmateix té un defecte que es veu en una sola línia:

console.log(t1.descripcio === t6.descripcio);   // false

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ò:

console.log(t1.descripcio === t6.descripcio);   // true  ← una sola funció, compartida

I per aconseguir-ho cal entendre el mecanisme de cerca de propietats de JavaScript.

  1. Cada objecte té un enllaç [[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é ell

Fixa'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.

  1. La cadena de prototips: com es resol l'accés a una propietat

Quan escrius objecte.propietat, el motor segueix aquest algorisme:

  1. objecte una propietat pròpia anomenada propietat? Si sí, en retorna el valor i acaba.
  2. Si no, pren el [[Prototype]] d'objecte. És null? Aleshores retorna undefined i acaba.
  3. 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'));      // false

Això 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: llegeix etiquetes de la cadena i modifica aquell array, que veuen tots. Per això els prototips han de contenir mètodes, no dades mutables.

  1. Llegir i escriure el prototip: 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 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 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.

  1. Object.create: crear un objecte amb el prototip que tu diguis

Object.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 res

Aquest 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.

  1. La propietat prototype de les funcions

Aquí 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

  1. Què fa new exactament: els quatre passos

Ara sí, tanquem el deute de 04-02. Quan escrius new Tasca(1, 'Redissenyar…'), el motor fa quatre coses en aquest ordre:

  1. Crea un objecte buit.
  2. Enllaça el seu [[Prototype]] a Tasca.prototype.
  3. Executa el cos de Tasca amb this apuntant a aquest objecte nou, passant-li els arguments.
  4. 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);             // true

Que 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

  1. El constructor Tasca i els seus mètodes a Tasca.prototype

Amb 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 acomplert

Amb 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));                 // 124

Els 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.

  1. 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 fusteria

Cap 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.

  1. instanceof, constructor i isPrototypeOf

Ja 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);      // false

Els 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.

  1. 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 cadena

La 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') No
obj.hasOwnProperty('x') No (però el mètode sí que s'hereta; Object.hasOwn és la versió moderna i segura)
'x' in obj

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òpies

Per 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:

for (const clau in p) {
  if (!Object.hasOwn(p, clau)) continue;
  console.log(clau);        // només 'x'
}

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.

  1. 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);              // true

Els dos passos mereixen que els miris per separat, perquè són la font de gairebé tots els errors d'aquest tema:

  1. Tasca.call(this, dades) és el call de 04-02 posat a treballar. Executa el cos del constructor pare amb this apuntant 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.
  2. Object.create(Tasca.prototype) crea un objecte buit enllaçat al prototip del pare, i el posa com a prototype del fill. Un error freqüentíssim és escriure TascaRecurrent.prototype = Tasca.prototype (sense Object.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.

  1. Object.prototype i per què tot objecte té toString

Al 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);   // true

Aquell '[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.prototypeObject.prototypenull
'hola' (en accedir a un mètode) String.prototypeObject.prototypenull
function f(){} Function.prototypeObject.prototypenull
new Map() Map.prototypeObject.prototypenull
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.

  1. 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.ultim amb 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.flatten va trencar pàgines senceres quan l'estàndard va incorporar flat. 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 qualsevol for...in sobre 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));     // 3

La 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 prototype amb [[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 un TypeError immediat perquè this és undefined; 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 un push des 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 prototype sencer (Tasca.prototype = { … }) trenca la propietat constructor i, si ja havies creat instàncies, aquestes continuen enllaçades a l'objecte antic. Afegeix mètodes un a un, o restaura constructor explícitament.
  • Oblidar Pare.call(this, …) en heretar: la instància filla té els mètodes però cap dada, i tot surt undefined. És l'error número u de l'herència manual.
  • Escriure Fill.prototype = Pare.prototype en lloc d'Object.create(Pare.prototype): comparteixen objecte, i els mètodes del fill es filtren al pare.
  • Fer servir Object.setPrototypeOf en calent. És correcte però lent; decideix el prototip en crear l'objecte.
  • Refiar-se d'obj.constructor per 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 retorni true si this.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òpia

El 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);      // true

Els 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.prototypemap, 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

Mòdul 2: Estructures de Control

Mòdul 3: Funcions

Mòdul 4: Objectes i Arrays

Mòdul 5: Objectes i Funcions Avançades

Mòdul 6: El Model d'Objectes del Document (DOM)

Mòdul 7: APIs del Navegador i Temes Avançats

Mòdul 8: Proves i Depuració

Mòdul 9: Rendiment i Optimització

Mòdul 10: Frameworks i Llibreries de JavaScript

Mòdul 11: Projecte Final

© Copyright 2026. Tots els drets reservats