JavaScript prototipsko nasleđivanje

Do sada smo naučili puno toga o načinima kako JavaScript simulira objektno-orijentisano programiranje. Međutim, svi ti načini su pokušaji "krpljenja" rupe koja zaista ne postoji. Objekti u JavaScriptu imaju ugrađeni mehanizam nasleđivanja kroz takozvano prototipsko nasleđivanje. To je poseban stil objektno-orijentisanog programiranja koji čak nije ni jedinstven za JavaScript (mada sumnjamo da ste baš upoznati sa jezicima kao što su Self, Lua, Cecil i sličnim).

U klasičnom OOP-u, imamo klase kao tipove i objekte kao "instance" tih klasa. Međutim, neki jezici (između ostalih i JavaScript) imaju samo objekte, ne i klase. U takvim jezicima se onda koristi ovaj tip nasleđivanja.

Prototipsko nasleđivanje se zasniva na delegiranju. Svaki objekat ima svoja svojstva i metode, ali i posebnu vezu ka roditeljskom objektu koji onda predstavlja njegov prototip od koga nasleđuje svojstva i metode. Suvišno je reći - i taj roditeljski objekat takođe ima svoj prototip i tako dalje. Kada pristupamo npr. određenom metodu objekta, ako ga objekat nema, on se "traži" u njegovom prototipu, pa u prototipu prototipa i tako sve do kraja lanca.

Inače u JavaScriptu svi objekti u krajnjoj liniji potiču od objekta Object. Da vidimo kako se prototipsko nasleđivanje implementira u JavaScriptu.

Svaki objekat u JavaScriptu ima specijalnu referencu __proto__ (tzv. "dunder proto", skraćeno od "double underscore proto") koja pokazuje na roditelj-objekat od koga taj objekat nasleđuje svojstva i metode. Međutim ovo svojstvo nije po standardu i ne bismo trebali da ga koristimo kako bismo definisali nasleđivanje.

Da bismo "izvukli" objekat čija svojstva nasleđuje naš objekat, koristimo metod getPrototypeOf() objekta Object. Ovo su dve iste stvari, ali prvi način je "zvanično" ispravan:

Object.getPrototypeOf(obj) obj.__proto__

Da bi objekat bio povezan u prototipski lanac nasleđivanja, moramo nekako da kontrolišemo na šta pokazuje referenca __proto__. Kada je objekat jednom povezan, moći će da pristupa svojstvima i metodima svojih "predaka", i to čak iako se u prototip tek kasnije dodaju i neka nova svojstva.

U ovom tekstu ćemo obraditi "stariju" verziju prototipskog nasleđivanja, korišćenjem konstruktorske funkcije. U tekstu o novijem načinu kreiranja objekata pomoću objekta Object, ćemo obraditi drugi način prototipskog ulančavanja.

Sa funkcijama-konstruktorima smo se već upoznali ranije u ovom odeljku. Znamo da su to praktično obične funkcije, koje definišu oblast važenja i postavljaju svojstva novoformiranog objekta. Takođe smo se upoznali i sa primitivnijom simulacijom nasleđivanja sa konstruktorskim funkcijama, bez prototipa.

Od ranije znamo da su i funkcije objekti, i da imaju svoja svojstva i metode. Funkcije, za slučaj da se koriste kao konstruktori, uvek imaju specijalno svojstvo - referencu prototype, koja u stvari predstavlja prototipski objekat za sve objekte koje kreiramo pozivajući funkciju kao konstruktor. Drugim rečima, pozivanje operatora new nad funkcijom radi još jednu dodatnu stvar koju do sada nismo znali:

obj = new Fun(); // posledica ovoga je: obj.__proto__ === Fun.prototype

Na taj način smo u stvari "podesili" šta će biti prototip objekta. Naš savet je da u prototipu definišete samo metode, dok svojstva objekta definišete u konstruktorskoj funkciji.

function Fun() { this.svojstvo = vrednost; this.svojstvo = vrednost; ... } Fun.prototype.metod = function() {}; Fun.prototype.metod = function() {}; ...

Na ovaj način imamo "čistu" situaciju, da svakom objektu "pripadaju" njegova svojstva, ali metode "povlači" iz jedinstvenog prototipa. Znači da su metode zajedničke svim objektima koji su nastali na osnovu iste konstruktorske funkcije, ali svojstva (podaci) nisu.

Naravno, mi možemo definisati i svojstva u prototipu, ali treba da znate da čim promenite vrednost nekog svojstva, to se neće desiti u prototipskom objektu, već će se kreirati novo svojstvo samog objekta. Na taj način JavaScript štiti ostale objekte, da se ne bi desilo da jedan objekat utiče na prototip svih ostalih objekata.

Ovo je način kako objekti nasleđuju svojstva i metode svoje "klase". Međutim, pravo nasleđivanje je kada imamo jednu opštu klasu koja definiše neka generalna svojstva i metode, a iz nje onda razvijamo specifične klase.

E, sad ide zvrčka. Svaka funkcija, pa i konstruktorska, nasleđuje osobine objekta Function. Naravno, to što ona nasleđuje nema nikakve veze sa objektom koji se kreira tom funkcijom. Za razliku od OOP-a, gde se napravi klasa, a onda se objekti prave "po modli" te klase (pasivno), u JavaScriptu se napravi funkcija koja pravi objekat (aktivno).

Šta je funkcija nasledila, bitno je samo "u životu i radu" te same funkcije, ne i njenih objekata. Objektima je bitno šta im funkcija pruža, a to što im pruža, funkcija radi na dva načina:

Princip prototipskog nasleđivanja

Znači lanac nasleđivanja se ne dešava sam od sebe! Opšta i specifična konstruktorska funkcija nemaju veze jedna sa drugom, u smislu da "jedna nastaje na osnovu prethodne", kao što je to slučaj sa klasama u OOP-u. Šta onda treba da uradimo da bismo omogućili da objekat koristi i svojstva i metode opšte konstruktorske funkcije? Pa, potrebno je da ih "spustimo" u prototip specifične kostruktorske funkcije! Drugim rečima treba da napravimo sledeću konstrukciju:

function Opsta() { this.svojstvo_A = vrednost; ... } Opsta.prototype.metod_A = function() {}; Opsta.prototype.metod_B = function() {}; ... function Specificna() { this.svojstvo_X = vrednost; ... } Specificna.prototype = new Opsta(); Specificna.prototype.metod_X = function() {}; Specificna.prototype.metod_B = function() {}; ... var obj = new Specificna();

Kakva je sada situacija? Vidimo da smo kreirali objekat obj korišćenjem operatora new nad funkcijom Specificna(). To je formiralo objekat čija referenca __proto__ pokazuje ka objektu Specificna.prototype. Pošto smo kreirali prototype objekat funkcije Specificna(), korišćenjem operatora new nad funkcijom Opsta(), samim tim smo povezali njegovu referencu __proto__ za objekat Opsta.prototype. U stvari, tako formiramo lanac nasleđivanja - kao "prototip od prototipa".

Kada se u programu zahteva određeni metod ili svojstvo objekta, prvo se gleda ono što pripada "lično" objektu. Ako se ne nađe, pretražuje se prototip objekta, ako ga nema ni tu, onda prototip prototipa, sve dok se na kraju ne stigne do objekta Object.prototype koji je ultimativni prototip svih objekata. Da vidimo šta sve ima objekat iz primera, i odakle mu:

obj.svojstvo_X // pripada objektu obj.metod_X // Specificna.prototype obj.svojstvo_A // Specificna.prototype obj.metod_A // Opsta.prototype obj.metod_B // Specificna.prototype

Ok, kako je onda svojstvo_A iz Specificna.prototype kad je definisana u funkciji Opsta()? Lako - ovo svojstvo je definisano kao this.svojstvo_A, što znači da se dodeljuje kao "lično" svojstvo objekta kada se on napravi operatorom new Opsta(). A sećate se da smo na taj način napravili objekat Specificna.prototype.

A kako to da se metod metod_B poziva isto iz objekta Specificna.prototype? Pa zato što smo ga posle kreiranja tog objekta redefinisali, tako da je nova funkcija "pokrila" staru.

Grafički primer prototipskog nasleđivanja

Pristup pokrivenim metodima nadklase

Ostaje samo da se zapitamo, kako da pristupimo "starom" metodu iz nadklase. Zašto nam ovo treba? Pa, recimo da metod u nadklasi (opštoj) odrađuje neki opšti posao, a metod u podklasi (specifičnoj) dodaje još neke specijalne stvari. Poenta je da ne moramo da kopiramo kod iz nadklase u podklasu. Ono što bi nama trebalo je nešto kao:

function stariMetod() {...} function noviMetod() { stariMetod(); ... }

Jedini problem je što je u ovom slučaju stariMetod() sakriven u prototype objektu opšte funkcije. Evo jednog od mogućih načina da se to prevaziđe - možemo napraviti referencu na "prototip prototipa" i koristiti je svaki put kada nam zatreba "pregaženi" metod.

function Specificna() { this._super = Object.getPrototypeOf(Specificna.prototype); } Specificna.prototype.metod = function() { this._super.metod.call(this); ... };

Uf, a zašto sada petljamo i call(this)? Pa jednostavno, da bismo vezali poziv starog metoda za trenutni objekat, ako zatreba da se koriste njegova svojstva.

Primer prototipskog nasleđivanja

Da vidimo kako bi izgledalo nasleđivanje dobro nam poznatih robota, ali ovaj put u "prototipskom" izdanju.


  function Robot()
  {
    this.X = 0;  // pozicija robota
    this.Y = 0;

    this.name = "";     // ime robota - u "apstraktnoj klasi" je prazno
    this.energy = 100;  // energija
  }
  
  Robot.prototype.init = function(name) {
    // osnovna inicijalizacija - služi da se "popune svojstva"
    this.name = name;
  };

  Robot.prototype.charge = function() {
    // ...punjenje baterije
  };

  Robot.prototype.move = function(mX, mY) {
    // ...kretanje robota
  };

Najpre smo kreirali osnovnu klasu - Robot. Svaki robot će imati poziciju, energiju i ime. To će biti svojstva našeg objekta i njih ćemo smestiti unutar same konstruktorske funkcije.

Sa druge strane, metode ćemo dodati u prototip funkcije Robot. ovo ima smisla, pošto svi objekti napravljeni na osnovu ove funkcije, treba da dobiju "svoja" svojstva, a metodi bi trebali da im budu zajednički.

Obratite pažnju na metod init(), koji služi da određenim svojstvima dodelimo vrednosti. To radimo zato što "apstraktna" klasa neće biti korišćena za konkretne objekte, već samo da napravi prototip izvedene funkcije. Pošto prototip izgrađujemo samo jednom, besmisleno je zadavati vrednosti svojstava kao parametre konstruktorske funkcije. Tako svojstva definišemo kroz jednu ovakvu funkciju, tek kada napravimo objekat.


  function Battlebot(name, energy)
  {
    this.init(name, energy);   // zadajemo početne postavke, pozivajući nasleđeni metod init()
    this.MAX_ENERGY = 500;     // maksimalna moguća energija za robota
    this.ENERGY_CHUNK = 10;    // kada se puni, za ovoliko se puni
  }
  Battlebot.prototype = new Robot(); // odmah gradimo prototip funkcije kao objekat klase Robot

  Battlebot.prototype.init = function(name, energy) {   // pokrivamo originalni init() metod
    this.energy = energy;
    Object.getPrototypeOf(Object.getPrototypeOf(this)).init.call(this, name);  // poziv originalnog metoda
  };

  Battlebot.prototype.charge = function() {   // pokrivamo originalni charge() metod
    if (this.energy < this.MAX_ENERGY) {
      this.energy += this.ENERGY_CHUNK;
    }
  };

  Battlebot.prototype.fire = function() {    // novi metod fire() - to je specifično za borbenog robota
    // ..."Destroy! Destroy!"
  };

Ovo je izvedena klasa - Battlebot - borbeni robot. Kao i ranije, borbeni robot može sve što i običan robot, a dodaje i neke svoje mogućnosti. Čim se deklariše funkcija Battlebot(), treba da joj definišemo i prototip, kao objekat klase Robot. Dakle, prototip funkcije Battlebot() dobija svojstva koja se zadaju u konstruktoru Robot(), a prototip funkcije Robot() već ima svoje metode.

Kada smo izgradili prototip, tek onda možemo u njega da dodajemo i metode. Metod charge() jednostavno "pokriva" originalni metod iz prototipa funkcije Robot(), a metod fire() je novi metod.

Pogledajte sada šta smo uradili sa metodom init(). Želimo da tokom kreiranja objekta klase Battlebot izvršimo i inicijalizaciju, s tim što za objekat zadajemo dva parametra (ime i energiju). Kreirali smo novi init() metod, koji nas u normalnim okolnostima sprečava da "dosegnemo" staru verziju metoda iz osnovne klase Robot(). Međutim, sa prototipskim nasleđivanjem, ovo postaje moguće.

Zašto bismo radili tako nešto? Pa recimo da jedan deo posla koji odrađuje ovaj metod treba da se napravi u izvedenoj klasi, ali dobar deo posla je nešto što je već napravljeno u osnovnoj klasi Robot. Umesto da copy-pastujemo kod, mnogo je praktičnije da prosto pozovemo metod iz nadklase. Kao što vidite, moguće je, mada ne baš elegantno.

U metodu init(), posle postavljanja vrednosti svojstva .energy, pozivamo originalni metod init() iz nadklase. Koristimo metod objekta Object.getPrototypeOf(), koji vraća prototip zadatog objekta. E sad, tražimo prototip prototipa objekta this. Jednostavno je - this je konkretan objekat borbenog robota. Njegov prototip je zadat kao prototip funkcije Battlebot(), a tek prototip prototipa je prototipski objekat funkcije Robot() u kome je definisan originalni metod init(). Kao da to nije bilo dovoljno, moramo da ga pozovemo sa call() (ili apply()) kako bismo povezali konkretan objekat this za njega.

Auf. Srećom, kreiranje i korišćenje objekta u programu, ne donosi nikakve zastrašujuće novitete...


  // KREIRANJE OBJEKATA
  var b = new Battlebot("Johnny5", 400);

  // KORIŠĆENJE OBJEKATA
  b.charge();        // koristi se novi metod charge
  b.move(15, 20);    // nasleđeni metod za kretanje
  b.fire();          // ovo može samo battlebot

Ako vam se posle svega još nije zavrtelo u glavi, možete isprobati i konkretan primer.

js-oop-proto1-sr

Lokalne promenljive i privatni metodi

Verovatno primećujete da je pokušaj da programirate objektno-orijentisano u JavaScriptu ekvivalentno pokušaju da kontrolišete polunaduvani balon: kad stisnete na jednoj strani, naduvaće se na drugoj. Zaista, svaki od stilova koji usvojimo ima neke svoje nedostatke.

Način koji smo demonstrirali, sa prototipski deklarisanim metodama, nema ništa od mogućnosti da u objektu "sakrijemo" lokalne promenljive i funkcije. Prosto - ako su metodi "dodati" u prototip van funkcije, oni se (pošto su to u stvari anonimne funkcije) ne nalaze u njenoj oblasti važenja i samim tim nemamo mogućnosti da "profitiramo" od closure mehanizma, koji nam je omogućavao da imamo te "privatne" vrednosti.

Srećom, niko nam ne brani da prototipske metode deklarišemo unutar konstruktorske funkcije. Tako bi primer od malopre mogao ovako da se modifikuje:

function Specificna() { var lokalna; this.svojstvo_X = vrednost; ... Specificna.prototype.metod_X = function() {}; Specificna.prototype.metod_B = function() {}; ... function privatna() {} ... } Specificna.prototype = new Opsta(); var obj = new Specificna();

Ovo nije najsrećnije rešenje - svaki put kada kreiramo novi objekat, takođe "gradimo" prototype objekat. Međutim, tako metodi istovremeno postaju closure funkcije, što nam dosta znači.

Primećujete da smo definiciju prototipa funkcije Specificna() ostavili "napolju". Ovo je obavezno da se uradi, pošto je neophodno da imamo definisan prototype pre kreiranja objekta.

Ako bi ova definicija bila unutar funkcije, desila bi se tek kada se funkcija pozove - pri kreiranju objekta, ali tada je već kasno. U tom trenutku objekat je već povezan sa default prototipom. To što mi posle usmerimo referencu prototype ka novom objektu, ne znači ama baš ništa.

  1. S. Stefanov, K.C. Sharma (2013): Object-Oriented JavaScript, 2nd.ed, Packt Publishing, Birmingham
  2. Addy Osmani, Learning JavaScript Design Patterns
Svi elementi sajta Web'n'Study, osim onih za koje je navedeno da su u javnom vlasništvu, vlasništvo su autora i ne smeju se koristiti, u celosti ili delimično bez pismenog odobrenja autora. To uključuje tekstove, slike, ilustracije, animacije, prateći grafički materijal i programski kod.
Ovaj sajt koristi tehnologiju kolačića (cookies). Detaljnije o tome možete pročitati u tekstu o našoj politici privatnosti.