Deo zbornika Napredni Javaskript
Prototipsko nasleđivanje
Pre uvođenja ES6 klasa, nasleđivanje se u Javascriptu ručno implementiralo preko prototipa. Prototipsko nasleđivanje i dalje radi u pozadini, ali je nova sintaksa značajno olakšala stvari.
Podrazumevani način implementiranja nasleđivanja u Javascriptu je ulančavanje nasleđivanja pomoću prototipa, odnosno prototipsko ulančavanje. Konstruktorska funkcija ima svojstvo prototype, koje ukazuje na prototipski objekat. Kada se funkcija pozove sa operatorom new, novi objekat će biti kreiran i vraćen. Novi objekt ima vezu sa prototipskim objektom, što mu omogućava da se koristi njegovim metodima i svojstvima.
Preko veze novog objekta sa prototipom se kreiran lanac, koji se naziva prototipski lanac.
Prototipsko ulančavanje
Da bismo implementirali hijerarhiju, definisaćemo tri konstruktor funkcije:
function Oblik() {
this.ime = 'Oblik'
this.toString = function() {
return this.ime
}
}
function Oblik2D() {
this.ime = '2D oblik'
}
function Trougao(stranice, visina){
this.ime = 'Trougao'
this.stranice = stranice
this.visina = visina
this.izracunajPovrsinu = function() {
return this.stranice * this.visina / 2
}
}
Kod u kome nastaje „magija“ nasleđivanja je sledeći:
Oblik2D.prototype = new Oblik()
Trougao.prototype = new Oblik2D()
Sta se ovde dešava? Umesto da prototip funkcije Oblik2D proširimo pojedinačnim svojstvima, menjamo ga drugim objektom, kreiranim pomoću new Oblik(). Isto primenjujemo i na Trougao. Važno je da zapamtimo da JavaScript funkcioniše pomoću objekata, a ne pomoću klasa. Potrebno je kreirati instancu da se nasledi svojstvo - ne nasleđuje se direktno od konstruktora.
Zamena čitavog prototipa, suprotno dodavanju svojstava, ima neželjene efekte na svojstvo constructor. Zato je potrebno da ponovo postavimo constructor nakon nasleđivanja, na sledeći način:
Oblik2D.prototype.constructor = Oblik2D
Trougao.prototype.constructor = Trougao
Sada instanciranje objekta i pozivanje metoda funkcioniše na očekivani način:
const trokut = new Trougao(5, 10)
trokut.izracunajPovrsinu() // 25
Objekat trokut nema svoju metodu toString(), ali je nasleđuje. Primetimo da nasleđeni metod toString() povezuje this sa objektom trokut:
trokut.toString() // "Trougao"
Traženje po prototipskom lancu
Razmotrimo šta JavaScript virtuelna mašina radi kada pozovemo metod trokut.toString():
- Prolazi kroz sva svojstva
trokuti ne nalazi metod pod nazivomtoString(). - Pronalazi da je objekat na koji ukazuje svojstvo
trokut.__proto.__instanca konstruktoraOblik2D(). - JavaScript virtuelna mašina prolazi kroz instancu objekta
Oblik2Di ne nalazi metodtoString(). - Zatim, proverava svojstvo
__proto__tog objekta. Ovoga puta svojstvo__proto__ukazuje na instancu koju je kreirao konstruktorOblik(). - Instanca konstruktora
Oblik()je pregledana i metodtoString()je konačno pronađen. - Ovaj metod se poziva na objektu
trokut, athisiz konstruktoraOblikukazuje na objekattrokut.
Provera nasleđivanja
Ako zatražimo od objekta trokut da nam „saopšti“ svoj konstruktor, dobićemo tačan odgovor, jer smo svojstvo constructor ponovo postavili nakon nasleđivanja:
trokut.constructor === Trougao // true
Ako upotrebimo operator instanceof, možemo da potvrdimo da je objekat trokut instanca sva tri konstruktora:
trokut instanceof Oblik // true
trokut instanceof Oblik2D // true
trokut instanceof Trougao // true
Premeštanje zajedničkih svojstava u prototip
Kada kreiramo objekte koristeći konstruktor funkciju, sopstveni atributi se dodaju pomoću rezervisane reči this. To znači da, uvek kada kreiramo novi objekat pomoću new Oblik(), novo svojstvo ime se kreira i skladišti u memoriju. Ovo može biti neefikasno u slučajevima kada se atributi ne menjaju u instancama.
U prethodnom primeru Oblik() je definisan na sledeći način:
function Oblik(){
this.ime = 'Oblik'
}
Druga mogućnost je da se svojstvo ime doda u prototip i podeli između svih instanci:
function Oblik() {}
Oblik.prototype.ime = 'Oblik'
Tako se ne kreira svojstvo za svaki novi objekat, već koristi svojstvo iz prototipa. Ovaj koncept bi trebalo koristiti za sva svojstva koja se ne menjaju od jedne do druge instance. Metodi su idealni za ovu vrstu deljenja.
Sada ćemo refaktorisati prethodni primer, tako što ćemo sve metode i odgovarajuća svojstva premestiti u prototype. U metodima Oblik() i Oblik2D() se svi elementi mogu deliti:
function Oblik() {}
// prosirenje prototipa
Oblik.prototype.ime = 'Oblik'
Oblik.prototype.toString = function() {
return this.ime
}
function Oblik2D () {}
// nasledjivanje
Oblik2D.prototype = new Oblik()
Oblik2D.prototype.constructor = Oblik2D
// prosirenje prototipa
Oblik2D.prototype.ime = '2D oblik'
Kao što vidimo, moramo prvo da izvršimo nasleđivanje pre nego što proširimo prototip. Ako najpre ne izvršimo nasleđivanje, sve što dodamo u prototip Oblik2D.prototype se briše kada izvršimo nasleđivanje.
Konstruktor Trougao je malo drugačiji, zato što je svaka instanca može imati različite dimenzije. Dobro je da sopstvena svojstva stranice i visina zadržimo, a delimo ostalo:
function Trougao(stranice, visina) {
this.stranice = stranice
this.visina = visina
}
// nasledjivanje
Trougao.prototype = new Oblik2D()
Trougao.prototype.constructor = Trougao
// prosirenje prototipa
Trougao.prototype.ime = 'Trougao'
Trougao.prototype.izracunajPovrsinu = function() {
return this.stranice * this.visina / 2
}
hasOwnProperty()
Možemo da koristimo metod hasOwnProperty() da bismo videli razliku između sopstvenog svojstva i svojstva iz prototipskog lanca. Na primer:
trokut.hasOwnProperty('stranice') // true
trokut.hasOwnProperty('ime') // false
Nasleđivanje: ažuriran primer
Nasleđivanje objekta Oblik.prototype je bolje od nasleđivanja objekta koji kreira konstruktor new Oblik(), jer on daje samo sopstvena svojstva koja se ne mogu ponovo koristiti. Ovo je ažurirani kod:
function Oblik() {}
// prosirenje prototipa
Oblik.prototype.ime = 'Oblik'
Oblik.prototype.toString = function() {
return this.ime
}
function Oblik2D() {}
// nasledjivanje
Oblik2D.prototype = Oblik.prototype
Oblik2D.prototype.constructor = Oblik2D
// prosirenje prototipa
Oblik2D.prototype.ime = '2D oblik'
function Trougao(stranice, visina) {
this.stranice = stranice
this.visina = visina
}
// nasledjivanje
Trougao.prototype = Oblik2D.prototype
Trougao.prototype.constructor = Trougao
// prosirenje prototipa
Trougao.prototype.ime = 'Trougao'
Trougao.prototype.izracunajPovrsinu = function () {
return this.stranice * this.visina / 2
}
// proba
const trokut = new Trougao(5, 10)
console.log(trokut.izracunajPovrsinu())
console.log(trokut.toString())
Kopiranje prototipa je efikasnije, ali ima neželjene efekte. Kada podređeni element modifikuje prototip, svi ostali u lancu nasleđuju promene, jer svi ukazuju na isti objekat. Na primer:
Trougao.prototype.ime = 'Trougao'
Ako sada kreiramo instancu new Oblik(), svojstvo ime biće “Trougao”:
const o = new Oblik ()
o.ime // "Trougao"
Literatura
- Ved Antani, Stojan Stefanov, Objektno-orjentisan JavaScript, Beograd, 2017.