Deși prototipurile în javascript nu sunt un subiect ușor de digerat, înțelegerea lor face parte din arsenalul de bază al unui web developer experimentat. Așa că în cele ce urmează o să-ți prezint toate detaliile legate de ce sunt și cum funcționează ele.

Dacă ai folosit vreodată un breakpoint în JavaScript, și sunt sigur că ai făcut-o deja dacă mesajul ăsta ajunge la tine, atunci ai văzut că în momentul în care faci debug și încerci să inspectezi valorile unor variabile în contextul în care te afli, descoperi o grămadă de proprietăți, metode și alte definiții ciudate care nu știi ce sunt și de unde vin.

Ei bine toate detaliile alea vin din lanțul de prototipuri al obiectului inspectat. Știu, sună foarte complicat, dar în realitate chiar nu e.

Ce sunt prototipurile

Prototipurile reprezintă un mecanism prin care JavaScript permite moștenirea de caracteristici între obiecte.

Adică în cuvinte mai simple, lanțul de prototipuri reprezintă schița după care este structurat un obiect. Gândește-te la asta ca la un plan de proiectare sau o schiță după care se face execuția unei construcții sau a unui mecanism.

Planul ăsta conține toate detaliile necesare, până la cel mai mic detaliu, pentru construcția sau fabricarea unui produs finit. În cazul nostru, produsul finit fiind obiectul în sine.

Cum putem să aflăm detaliile prototipului unui obiect

Cea mai simplă modalitate pentru aflarea detaliilor legate de prototipul unui obiect este folosirea funcției Object.getPrototypeOf(object).

function Test() {
  console.log("test");
}

const t = new Test();

Object.getPrototypeOf(t);

O să obținem următorul răspuns:

{constructor: ƒ}

Mergem mai departe și desfășurăm răspunsul, observăm că avem o proprietate denumită [[Prototype]] .

Dacă desfășurăm proprietatea asta, o să vedem o listă de genul ăsta:

constructor: ƒ Object()
hasOwnProperty: ƒ hasOwnProperty()
isPrototypeOf: ƒ isPrototypeOf()
propertyIsEnumerable: ƒ propertyIsEnumerable()
toLocaleString: ƒ toLocaleString()
toString: ƒ toString()
valueOf: ƒ valueOf()
__defineGetter__: ƒ __defineGetter__()
__defineSetter__: ƒ __defineSetter__()
__lookupGetter__: ƒ __lookupGetter__()
__lookupSetter__: ƒ __lookupSetter__()
__proto__: Object
get __proto__: ƒ __proto__()
set __proto__: ƒ __proto__()

Unde __proto__: Object reprezintă prototipul obiectului inspectat. Adică schița care a fost folosită pentru crearea obiectului nostru a fost cea de Object , care este și prototipul rădăcină din lanț.

Lanțul de prototipuri

Așa cum intuiești din denumire, fiecare obiect are un prototip, iar fiecare prototip reprezintă în sine un alt obiect, care și el are la rândul lui un prototip. Asta înseamnă ce înseamnă de fapt?

Că fiecare obiect este suma comportamentelor și datelor tuturor prototipurilor care îl alcătuiesc.

Asta înseamnă că în exemplul de mai sus, putem apela toString() pe obiectul nostru, chiar dacă metoda asta nu a fost declarată nicăieri.

Hai să luăm următorul exemplu:

const person = {
  name: "Bogdan",
  present() {
    console.log(`Numele meu este ${this.name}`);
  },
};

Pe obiectul de mai sus, putem să apelăm present() , și vom obține răspunsul:

Numele meu este Bogdan

Dar putem să apelăm și toString() și vom obține:

"[object Object]";

De unde vine toString(), că noi n-am declarat-o nicăieri?

Așa cum ziceam anterior, fiecare obiect are un prototip, iar prototipul la rândul lui are și el alt prototip și tot așa.

În momentul în care încercăm să apelăm o metodă sau funcție se execută următorii pași:

  • Se caută metoda în obiectul în sine
  • Dacă nu este găsită, se caută aceeași metodă în prototipul obiectului
  • Dacă nici așa nu este găsită, se caută în prototipul prototipului
  • Tot așa până când prototipul părinte ajunge să fie null , caz în care se returnează undefined dacă metoda nu este găsită

Același lucru este valabil și pentru proprietăți, nu doar metode.

Setarea prototipului unui obiect

JavaScript permite setarea prototipului unui obiect prin 2 metode:

  1. Prin folosirea Object.create
const personPrototype = {
  name: "Bogdan",
  present() {
    console.log(`Numele meu este ${this.name}`);
  },
};

const p = Object.create(personPrototype);
p.present();

//rezultat: Numele meu este Bogdan

Această metodă ne permite crearea unui obiect și setarea prototipului acestuia, în cazul ăsta personPrototype . Astfel că ulterior, pe obiectul creat putem apela present() , care va executa implementarea din personPrototype. 2. Prin folosirea Object.assign

const carPrototype = {
  describe() {
    console.log(`Masina este marca ${this.brand}`);
  },
};

function Car(brand) {
  this.brand = brand;
}

Object.assign(Car.prototype, carPrototype);

const car = new Car("BMW");
car.describe();

//rezultat: Masina este marca BMW

În felul ăsta, toate obiectele create folosind funcția Car ca și constructor, vor avea carPrototype ca prototip și implicit și metoda describe().

Concluzie

În încheiere hai să facem o scurtă recapitulare.

  • Prototipurile sunt schițe folosite pentru crearea obiectelor
  • Fiecare obiect are un prototip, care la rândului are un prototip, până la prototipul Object care este prototipul rădăcină. Adică nu mai are un alt prototip la rândul lui (valoarea prototipului lui Object este null ). Astfel se formează ceea ce se numește “lanțul de prototipuri”
  • Sunt 2 moduri prin care poți seta prototipul unui obiect, fie prin Object.create, caz în care se crează obiectul și i se atribuie prototipul. Sau prin Object.assign, caz în care se crează obiectul folosind o funcție constructor, căreia i se specifică un prototip. Astfel toate obiectele viitoare create cu funcția respectivă vor avea prototipul specificat în momentul în care s-a apelat Object.assign.