JavaScript nasleđivanje sa funkcijom klase
Jedan od najvažnijih koncepata OOP-a je nasleđivanje, odnosno mogućnost da na osnovu neke klase kreiramo drugu klasu koja preuzima sve osobine prethodne klase (nasleđuje je), ali je istovremeno proširuje, odnosno "konkretizuje". Ta prethodna klasa se naziva roditejska (parent), nadklasa ili osnovna klasa. Nova klasa koja se pravi na osnovu nje, naziva se podklasa ili izvedena klasa.
Ovaj tekst se direktno nastavlja na članak o Konstruktorskoj funkciji, pa ako ste ga preskočili, predlažemo da se vratite na njega. Da biste do kraja razumeli o čemu pišemo, bitno je da ste shvatili neke koncepte JavaScript programiranja o kojima smo ranije pisali, konkretno Closure funkcije i Objekat this.
Tehnika konstruktorske funkcije nam dozvoljava da na neki način simuliramo ovo ponašanje. Sve što je potrebno je da na novokreirani objekat prvo "primenimo" funkciju nadklase, a onda radimo sve ostalo. Za konstruktorsku funkciju koja se poziva operatorom new, možemo naslediti osnovnu klasu na sledeći način:
function Klasa()
{
this.svojstvo = vrednost;
}
function Podklasa()
{
Klasa.call(this);
this.novo = vrednost;
}
Kako ovo funkcioniše? Kada kreiramo objekat izvedene klase, pozivamo funkciju Podklasa() u kontekstu objekta koji kreiramo. Znači, kao što smo već apsolvirali, unutar funkcije Podklasa(), objekat this predstavlja naš novi objekat. Sve što radimo je da mu na samom početku "prišljapčimo" svojstva i metode osnovne klase koju nam u ovom primeru predstavlja funkcija Klasa(). Drugim rečima, pozivamo funkciju Klasa() u kontekstu našeg novog objekta this, koristeći metod call(). Tako se ova funkcija izvrši i u objekat "ubaci" svoja svojstva, a onda se nastavlja izvršavanje funckije Podklasa() koja dodaje nova svojstva (i metode, naravno).
Objekat se kreira kao što smo već naučili:
var obj = new Podklasa();
Zašto insistiramo da se poziv osnovne funkcije uradi na početku? Jednostavno, da bismo imali prostora da pokrijemo neka stara svojstva i metode novim. To znači da posle "nasleđivanja", podklasa objektu može dodati svojstva sa nazivima koji već postoje u osnovnoj klasi. U tom slučaju, se vrednost svojstva koju je postavila osnovna klasa zanemaruje, a svojstvo dobija novu vrednost, tj. "referira" na novu funkciju - kažemo da "pokriva" ili "preskače" (override) stari metod.
Ova simulacija nasleđivanja funkcioniše sve dok ne poželimo da iskoristimo još jednu osobinu klasičnog objektno-orijentisanog programiranja, a to je mogućnost da pozovemo pokriveni metod iz osnovne klase, što je ovakvom tehnikom neizvodljivo.
Simulacija nasleđivanja sa konstruktorskom funkcijom
Ovde ćemo ilustrovati simulaciju nasleđivanja pomoću konstruktorskih funkcija. Najpre da napravimo funkciju klase. Ovo ne bi trebalo da nam bude problematično.
function Robot(name) // OSNOVNA "KLASA"
{
this.X = 0;
this.Y = 0;
this.name = name;
this.energy = 100;
this.charge = function() { // metod objekta
// ... punjenje baterije
};
this.move = function(mX, mY) { // metod objekta
// ... kretanje robota
};
}
Kreirali smo funkciju klase Robot(), koja obuhvata podatke i ponašanja koja ima svaki robot. Dodali smo svojstva X i Y koje označavaju poziciju robota, kao i svojstvo name i energy. Takođe smo kreirali i dva metoda charge() za punjenje baterije i move() za kretanje robota.
Sada ćemo kreirati klasu borbenog robota - Battlebot(). Borbeni robot bi trebao da može sve što i običan robot, ali uz neke izmene i dopune.
function Battlebot(name) // IZVEDENA "KLASA"
{
Robot.call(this, name); // nasleđujemo klasu Robot
var MAX_ENERGY = 500; // dodajemo i neke privatne promenljive
var ENERGY_CHUNK = 10;
this.energy = MAX_ENERGY; // zadajemo novu vrednost za svojstvo .energy
this.charge = function() { // pokrivamo originalni charge() metod
if (this.energy < MAX_ENERGY) {
this.energy += ENERGY_CHUNK;
}
};
this.fire = function() { // novi metod
console.log("Destroy! Destroy!");
};
}
Prva stvar koju radimo u konstruktorskoj funkciji, je da pozovemo funkciju osnovne klase, tj. funkciju Robot(). Kao prvi parametar obavezno zadajemo sam objekat koji će biti prosleđen i u koji će se dodati svojstva osnovne klase, a to je objekat this. Posle zadajemo sve ostale parametre, ako ih ima, a u ovom slučaju je to naziv robota.
Onda dodajemo lokalne promenljive koje su dostupne samo funkcijama unutar funkcije Battlebot(). Kada zadamo vrednost za svojstvo koje postoji u osnovnoj klasi (u ovom slučaju energy), svojstvo će prosto dobiti novu vrednost. Ako svojstvo predstavlja referencu na funkciju (tačnije metod), kao što je slučaj sa svojstvom charge, originalna funkcija koja je deklarisana u funkciji Robot() će biti zanemarena i umesto nje će se koristiti funkcija iz klase Battlebot.
Konačno, ova klasa dodaje i svoj metod fire. Tako će borbeni robot moći da se kreće i puni bateriju (kao i svaki robot), ali i da puca - što mogu samo borbeni roboti.
Što se tiče kreiranja i korišćenja objekta, nema nikakvih iznenađenja...
// KREIRANJE OBJEKATA
var b = new Battlebot("Johnny5");
// KORIŠĆENJE OBJEKATA
b.charge();
b.move(15, 20);
b.fire();
Prosto kreiramo objekat b, klase Battlebot() i prosleđujemo mu ime robota. Kada koristimo našeg robota, potpuno ravnopravno možemo da pristupamo nasleđenim metodima, kao i novim.
Pogledajte kako ovo funkcioniše na primeru:
Za drugi tip konstruktorske funkcije, koja sama kreira svoj objekat, nasleđivanje simuliramo na jako sličan način.
function Klasa()
{
return {
svojstvo: vrednost
};
}
function Podklasa()
{
var o = Klasa();
o.novo = vrednost;
return o;
}
Primećujete da se i ovde osnovna (početna) klasa razlikuje od svake izvedene. Objekat se inicijalno kreira samo u osnovnoj klasi, a posle se samo prenosi kroz izvedene klase. Tako kreiranom objektu u podklasi samo nadodajemo nova svojstva i metode i tako "dopunjen" objekat vraćamo kao rezultat.
Kreiranje objekta ne donosi nikakve novitete:
var obj = Podklasa();
Nasleđivanje sa funckijom koja kreira objekat
Ovaj primer samo ilustruje razlike između dva tipa konstruktorske funkcije. Napre kreiramo osnovnu klasu Robot().
function Robot(name) // OSNOVNA "KLASA"
{
return {
X: 0,
Y: 0,
name: name,
energy: 100,
charge: function() { // metod objekta
// ... punjenje baterije
},
move: function(mX, mY) { // metod objekta
// ... kretanje robota
}
};
}
Kreira se objektni literal, koji se odmah vraća kao rezultat funkcije. Objekat ima svojstva X, Y, name i energy, kao i dva metoda charge() i move() za kretanje robota.
Hajde da vidimo kako bismo sada napravili izvedenu klasu Battlebot().
function Battlebot(name) // IZVEDENA "KLASA"
{
var _self = Robot(name); // _self je objekat klase Robot
var MAX_ENERGY = 500; // privatne promenljive su i dalje tu
var ENERGY_CHUNK = 10;
_self.energy = MAX_ENERGY; // zadajemo novu vrednost za svojstvo .energy u objektu _self
_self.charge = function() { // pokrivamo originalni charge() metod
if (this.energy < MAX_ENERGY) {
this.energy += ENERGY_CHUNK;
}
};
_self.fire = function() { // novi metod
console.log("Destroy! Destroy!");
};
return _self; // bitna razlika - dopunjeni objekat se vraća kao rezultat
}
Primećujemo da ova funkcija klase mnogo više podseća na "klasičnu" konstuktorsku funkciju. Najbitnije razlike su u prvom i poslednjem redu. Na početku moramo da kreiramo objekat klase koju nasleđujemo. Zatim u taj objekat dodajemo svojstva i metode koje želimo, s tim što možemo i menjati već postojeće elemente objekta. Jedino što ovde ne koristimo objekat this, već _self koji smo sami kreirali. Na kraju, tako izmenjen objekat vraćamo kao rezultat.
Kreiranje objekta je malo drugačije, a njegovo korišćenje potpuno isto (a to je i poenta svega što radimo).
// KREIRANJE OBJEKATA
var b = Battlebot("Johnny5");
// KORIŠĆENJE OBJEKATA
b.charge();
b.move(15, 20);
b.fire();
Znači sve je isto, s tim što se sada, kod pravljenja objekta, ne koristi operator new.
Da pogledamo i primer...
Nema zaštićenih metoda
Ovako postignuto nasleđivanje u JavaScriptu ima još jedan problem. Nemoguće je napraviti tzv. zaštićene metode. Zaštićeni (protected) metodi su oni koji se ne mogu klasično pozvati kroz instancu (objekat), ali su dostupni unutar podklasa - praktično, kao privatni metodi koje možemo koristiti i u podklasi. Toga ovde nema. U svakoj konstruktorskoj funkciji možemo imati unutrašnje funkcije, ali one su strogo privatne - dostupne su samo metodima (funkcijama) iz te konstruktorske funkcije.
Slabe strane nasleđivanja
Da vidimo jednu ilustraciju problema na koji možemo da naiđemo radeći sa privatnim funkcijama i promenljivama. Pogledajte primer osnovne klase Robot.
function Robot() // OSNOVNA "KLASA"
{
var energy = 100;
this.charge = function() {
battery(1);
};
this.level = function() {
console.log(energy);
};
function battery(num) { // unutrašnja funkcija - privatni metod
energy += num;
}
}
U ovom slučaju ne želimo da energija robota bude javno dostupna, pa je beležimo u privatnoj promenljivoj energy. Metod charge() interno poziva unutrašnju (privatnu) funkciju battery(), koja na količinu energije dodaje zadatu vrednost (vrednost može biti i negativna i u tom slučaju se energija smanjuje).
Takođe imamo i metod level() koji ispisuje trenutnu količinu energije koju robot ima. Dakle situacija je takva da u dva metoda koristimo privatnu funkciju i lokalnu promenljivu.
Pogledajte sada šta se dešava kada klasa Battlebot nasledi klasu Robot i pokuša da pristupi privatnim elementima.
function Battlebot() // IZVEDENA "KLASA"
{
Robot.call(this);
var energy = 500; // ovo energy nema veze sa promenljivom iz osnovne klase
this.fire = function() {
battery(-2); // GREŠKA - funkcija battery ovde nije dostupna
}
}
U klasi Battlebot smo dodali metod fire() koji šta-god-da-radi, takođe i umanjuje ukupnu energiju robota. Ovo je slučaj kada bismo želeli da iskoristimo unutrašnju funkciju napravljnu u osnovnoj klasi. Međutim, ovako izveden metod fire() bi izazvao grešku. U izvedenoj klasi ne postoji funkcija battery() - nju mogu koristiti samo metodi iz nadklase. Zašto? Pa jednostavno - to nisu "pravi" metodi već samo anonimne funkcije koje su referencirane svojstvima. Te anonimne funkcije su preko closure mehanizma vezane za funckije i promenljive koje su definisane u okviru njihove nadfunkcije (Robot).
Ista stvar se dešava ako u podklasi definišemo novu količinu energije u promenljivoj energy. Ako bismo za borbenog robota sada pozvali metod level() koji bi trebao da nam ispiše koliko energije ima robot, dobili bismo vrednost istoimene promenljive, ali koja "postoji" unutar oblasti važenja kojoj pripada i funkcija koju referencira level.
// KREIRANJE OBJEKATA
var b = new Battlebot();
// KORIŠĆENJE OBJEKATA
b.charge(); // radi normalno, jer to je nasleđeni metod
b.level(); // 101 - vezano je za privatnu promenljivu osnovne klase
Tako će rezultat posle jednog punjenja biti 101, a ne 501 kako smo mislili.
Dakle, da rezimiramo - ova tehnika omogućava da simuliramo klase i nasleđivanje, pa i čitav lanac nasleđivanja preko celog niza izvedenih klasa koje nasleđuju jedna drugu, ali ima i neke nedostatke u odnosu na klasično OOP:
- metodi se kreiraju iznova za svaki objekat - veće zauzeće memorije (iako moderni web čitači rade jako dobar posao memorijske optimizacije)
- pokrivanje metoda je lako ostvarivo, ali tada se ne može pozvati "originalni" metod nadklase
- ne postoji mogućnost kreiranja "zaštićenih" (protected) metoda i svojstava, već samo javnih i privatnih - metodi izvedene klase ne mogu pristupati privatnim funkcijama i promenljivama nadklase
Neki autori smatraju da bi JavaScript programeri svakako trebali da se rastanu sa klasičnim OOP načinom razmišljanja. Jedna od preporuka je da se napusti koncept "vidljivosti" elemenata objekta i da se ne insistira na konstruisanju objekata sa privatnim funkcijama i promenljivama, pošto je to ionako samo simulacija koja nastaje zahvaljujući closure-ima.
Prema tim mišljenjima, mnogo je bolja varijanta kreirati objekte sa svim "javnim" metodima i svojstvima, uz dobru dokumentaciju i imenovanje svojstava, kako programeri koji koriste te objekte ne bi radili sa njima nešto "nelegalno".
- S. Stefanov, K.C. Sharma (2013): Object-Oriented JavaScript, 2nd.ed, Packt Publishing, Birmingham
- Addy Osmani, Learning JavaScript Design Patterns