Els Object
En ES5 (abans de la sintaxi class), un objecte (en el sentit d'una "instància" d'un "plànol") és el resultat de cridar una funció constructora utilitzant la paraula clau new. ES5 (ECMAScript 5) es va publicar l'any 2009 i va ser l'estàndard de JavaScript durant molts anys fins a l'arribada d'ES6 (ES2015) el 2015.
Aquest objecte és una col·lecció de propietats que té dues parts:
-
Propietats Pròpies (Own Properties): Les dades úniques d'aquesta instància.
-
Un Enllaç a un Prototip: Un enllaç intern a un objecte compartit (el
prototypedel constructor) on resideixen tots els mètodes.
Abans d'ES6, no teníem la paraula clau class. Tot es feia amb funcions i un objecte especial anomenat prototype.
Un objecte ES5
Anem a descriure l'objecte usuari1 creat a partir d'un constructor Usuari.
Definició
Primer, necessitem la definició (el que ara anomenem "classe"):
// 1. LA FUNCIÓ CONSTRUCTORA
// (Defineix les propietats PRÒPIES que tindrà l'objecte)
function Usuari(nom, edat) {
// 'this' es refereix al nou objecte que s'està creant
this.nom = nom;
this.edat = edat;
}
// 2. EL PROTOTIP
// (Defineix els mètodes COMPARTITS)
Usuari.prototype.saludar = function() {
console.log("Hola, sóc " + this.nom);
};
Instanciació
Ara creem l'objecte:
let usuari1 = new Usuari("Anna", 30);
Objecte resultant (usuari1)
L'objecte usuari1 que s'acaba de crear NO conté el mètode saludar. Si miressis usuari1 per dins, veuriem això:
{
// 1. PROPIETATS PRÒPIES
// Aquestes propietats existeixen FÍSICAMENT dins de 'usuari1'
nom: "Anna",
edat: 30,
// 2. ENLLAÇ AL PROTOTIP (Amagat)
// Això és un enllaç intern, no una propietat visible directament.
// Tots els navegadors antics ho exposaven com '__proto__'
__proto__: Usuari.prototype
}
Mentre que Usuari.prototype és un objecte separat que conté:
{
saludar: function() { ... },
constructor: Usuari // (Una referència de tornada al constructor)
}
Com funciona? Cadena de prototips
Quan tu crides a usuari1.saludar(), passa el següent:
-
JavaScript busca la propietat
saludardins de l'objecteusuari1. -
No la troba (allà només hi ha
nomiedat). -
Com que no la troba, segueix l'enllaç intern (
__proto__) i va a buscar aUsuari.prototype. -
Allà sí que troba
saludar! -
Executa la funció
saludarestablint elthisperquè apunti ausuari1.
En resum, un objecte ES5 és un contenidor de dades úniques amb un enllaç a un prototip que conté els mètodes compartits. Com es feia cada part:
El "Constructor"
En lloc d'una class amb un mètode constructor(), simplement creàvem una funció normal. Per convenció, la posàvem amb la primera lletra en majúscula per indicar que estava pensada per ser utilitzada amb new.
// Aquesta funció ÉS el constructor.
function Cotxe(marca, any) {
// ...
}
Les propietats
Les propietats (les dades úniques de cada objecte) es declaraven dins de la funció constructora utilitzant this. Quan cridaves new Cotxe(...), JavaScript creava un objecte buit ({}) i l'assignava a this dins d'aquesta funció. La feina de la funció era "omplir" aquest this.
function Cotxe(marca, any) {
// Propietats úniques per a cada instància
this.marca = marca;
this.any = any;
this.velocitat = 0;
}
// En crear un objecte, s'executa aquesta funció:
let elMeuCotxe = new Cotxe("Ford", 2014);
// elMeuCotxe ara és: { marca: "Ford", any: 2014, velocitat: 0 }
Els mètodes (prototype)
Aquí ve la part més important. NO volíem definir els mètodes dins del constructor (com this.arrencar = function() {}), perquè això crearia una còpia de la funció per a cada cotxe creat (un malbaratament de memòria). Per compartir mètodes entre totes les instàncies, els afegíem a l'objecte prototype del constructor.
Què és el prototype? Tota funció de JavaScript té una propietat anomenada prototype, que és un objecte buit. Quan crees una instància amb new, aquesta instància "s'enllaça" secretament amb aquest objecte prototype.
// Afegim un mètode al 'prototype' de Cotxe
Cotxe.prototype.arrencar = function() {
console.log(`El ${this.marca} s'ha engegat!`);
};
Cotxe.prototype.frenar = function() {
this.velocitat -= 5;
console.log(`Velocitat: ${this.velocitat}`);
};
Com funciona?
-
Creem
let elMeuCotxe = new Cotxe("Ford", 2014);. -
Cridem
elMeuCotxe.arrencar(). -
JavaScript busca
arrencaraelMeuCotxe. No el troba (allà només hi hamarca,anyivelocitat). -
Com que no el troba, busca al
prototypeenllaçat (Cotxe.prototype). -
Allà sí que el troba! L'executa, i
thisapunta correctament aelMeuCotxe.
Això s'anomena cadena de prototips (prototype chain) i és el cor de l'herència a JavaScript.
Getters i setters
Abans d'ES6, això era més complicat. No teníem la sintaxi elegant get edat() {}. S'havia de fer servir un mètode estàtic de l'objecte Object anomenat Object.defineProperty().
També s'havien de definir al prototype.
// Volem una propietat "descripcio" que sigui calculada (un GETTER)
Object.defineProperty(Cotxe.prototype, 'descripcio', {
// Aquest 'get' s'executa quan algú fa: elMeuCotxe.descripcio
get: function() {
return `${this.marca} de l'any ${this.any}`;
},
enumerable: true // (perquè aparegui en bucles, etc.)
});
// Volem una propietat "any" que validi (un SETTER)
// (Nota: per això necessitem una variable interna _any)
function Cotxe(marca, any) {
this.marca = marca;
this._any = any; // Variable interna
}
Object.defineProperty(Cotxe.prototype, 'any', {
get: function() {
return this._any;
},
set: function(nouAny) {
if (nouAny > 2025) {
console.error("Aquest any encara no ha arribat!");
} else {
this._any = nouAny;
}
},
enumerable: true
});
Comparativa
El model de class d'ES6 és només una millora per sobre d'aquest model de prototype que acabem de veure.
| Concepte | "Classe" ES6 (Modern) | Funció Constructora (Antic) |
| Constructor | class Cotxe { constructor() {} } |
function Cotxe() {} |
| Propietats | constructor() { this.marca = ... } |
function Cotxe() { this.marca = ... } |
| Mètodes | class Cotxe { arrencar() {} } |
Cotxe.prototype.arrencar = function() {} |
| Getters/Setters | class Cotxe { get edat() {} } |
Object.defineProperty(Cotxe.prototype, ...) |