Konstruktorska funkcija za JavaScript objekte
Programeri koji imaju dosta iskustva u klasičnim objektno-orijentisanim jezicima, obično "ulaze" u JavaScript objektno programiranje preko konstruktorske funkcije. Zašto je ovaj način rada sa objektima toliko omiljen "pravim" programerima?
Kao što (verovatno) znate, u objektno-orijentisanom jezicima, programeri prave tzv. klase, koje predstavljaju praktično "modle" za objekte. To su kompleksni tipovi podataka, gde se deklarišu svojstva i metodi koji će kasnije pripadati objektu. Sa samom klasom, program ne može ništa da "uradi", ali se na osnovu klase u programu kreiraju objekti, koji predstavljaju konkretne "instance klase" (ako baš hoćemo da budemo tehnički precizni).
JavaScript ima tzv. besklasno programiranje, gde se samo kreiraju objekti, bez potrebe da se pravi klasa. U odnosu na klasičan OOP, ovakav način programiranja deluje kao totalni haos - objekti su potpuno nezavisni, nemaju ništa zajedničko, tokom rada programa im se mogu dodavati i brisati svojstva... Ovde ćemo pokazati način da se taj problem donekle prevaziđe.
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. A ne bi bilo loše i da se podsetite Oblasti važenja u JavaScriptu.
Konstruktorska funkcija kojoj se zadaje objekat
Konstruktorska funkcija je nešto najbliže klasi u JavaScriptu, tako da se još naziva i funkcija klase. To nije neka posebna vrsta funkcije, već prosto tehnika programiranja. O čemu se radi? U pitanju je funkcija koja definiše objekat. Pošto se svaki put neki novi objekat definiše na isti način, (dobija ista svojstva) ova funkcija efektivno "glumi" klasu. Ovu tehniku kreiranja objekta neki autori poistovećuju sa šablonom "Fabrika" (Factory pattern), mada bi, na osnovu literature, ovo pre bio šablon "Konstruktor" (Constructor pattern).
Konstruktorska funkcija se deklariše kao i bilo koja druga funkcija. Međutim, u samoj funkciji je značajna upotreba objekta this. Kao što znamo, this predstavlja objekat za koji je funkcija pozvana (tačnije "kontekst" izvršavanja funkcije). U ovom slučaju, this je objekat koji pravimo. Svako svojstvo koje želimo da kreiramo, definišemo kao this.svojstvo (na isti način možemo definisati i metod).
function Klasa()
{
var promenljiva = vrednost;
this.svojstvo = vrednost;
this.metod = function() {...};
function fun() {...}
}
Ok, šta onda unutar konstruktorske funkcije traže obične unutrašnje funkcije i promenljive? Jedna od velikih prednosti ove tehnike je što možemo simulirati privatne metode. To su funkcije koje nisu dostupne van objekta, već samo u "lokalu". Znači, funkcije koje se pozivaju kao metodi objekta, definišemo u formi "this.metod", dok ono što je "skriveno", kao obične funkcije. Potpuno ista stvar važi i za svojstva/promenljive. Ovo je omogućeno zahvaljujući closure mehanizmu u JavaScriptu, pomoću čega funkcije zadržavaju pristup svom "lokalnom" prostoru unutar koga su deklarisane.
Kada imamo konstruktorsku funkciju, svaki novi objekat pravimo na sledeći način:
var obj = new Klasa();
Kako ovo funkcioniše? Kao što vidite, konstruktorska funkcija se poziva pod operatorom new. To znači da se kreira novi "prazan" objekat, a onda se konstruktorska funkcija poziva za njega. Odatle u funkciji koristimo this.nešto za dodavanje svojstava/metoda objektu. Na taj način se funkcije, kao metodi objekta, "izvuku" van svoje oblasti važenja (konstruktorske funkcije), ali zadržavaju pristup svim unutrašnjim promenljivama i funkcijama kao closure-i. Onda se svojstva/metodi mogu koristiti u našem sveže kreiranom objektu:
obj.svojstvo = vrednost;
obj.metod();
Kao što vidimo, sasvim je jednostavno. Ne zaboravite, pristup je omogućen samo svojstvima (i metodima) objekta, ali ne i njegovim "unutrašnjim" funkcijama i promenljivama.
Funkcija klase
Evo jednog prostog primera čija je svrha da ispitamo kako funkcioniše kreiranje objekta pomoću konstruktorske funkcije i pristup promenljivama/svojstvima. Najpre ćemo deklarisati funkciju klase tj. konstruktorsku funkciju.
U pitanju je klasa Robot. Svaki objekat robota koji napravimo pomoću ove klase, treba da ima ime (name) i energiju (energy), kao i mogućnost da se puni (charge) i kreće (move). Prva dva su svojstva, a duga dva metodi objekta. Pogledajmo osnovni kostur:
function Robot(name)
{
this.name = name; // svojstvo name dobija vrednost iz parametra pri pozivu funkcije
this.energy = 100; // svojstvo energy dobija početnu vrednost 100
this.charge = function() { // metod objekta
// robot puni bateriju
};
this.move = function(mX, mY) { // metod objekta
// robot se kreće za mX jedinica levo-desno i mY jedinica napred-nazad
};
}
var r = new Robot("R2D2"); // KREIRANJE OBJEKTA
Hajde sada da malo "raščerečimo" ovu tehniku. Vidimo dve glavne stvari: deklaraciju funkcije Robot() i kreiranje objekta r. Funkcija Robot() nam praktično "glumi" klasu, pa smo je u tu čast nazvali velikim početnim slovom.
Prva naredba koja će se izvršiti je new Robot(...), koja predstavlja kreiranje objekta. Funkcioniše tako što se kreira novi prazan objekat, a onda se za njega poziva funkcija Robot(). To u stvari znači da će objekat this unutar funkcije Robot() biti taj naš novi objekat.
Zahvaljujući tome možemo definisati svojstva objekta this. To znači da kad otkucamo "this.nešto", to znači da našem objektu koji konstruišemo, "prilepimo" neko novo svojstvo. To svojstvo može imati običnu vrednost, a može biti i referenca na funkciju (tačnije referenca na bilo kakav objekat). U ovom primeru smo napravili reference na anonimne funkcije this.charge (punjenje robota) i this.move (kretanje robota). Ova svojstva se onda mogu koristiti kao metodi našeg objekta.
Promenljiva r postaje referenca na taj objekat i kasnije pomoću nje pristupamo svojstvima i metodima objekta.
// KORIŠĆENJE OBJEKTA
r.charge(); // punjenje
r.move(15, 20); // pomeranje za 15 jedinica u stranu i 20 jedinica napred
r.energy = 500; // možemo direktno zadati vrednost svojstva
Kada se poziva konstruktor Robot(), prosleđuje se ime robota ("R2D2"). Pošto je ovo na kraju krajeva, jedna obična funkcija, naravno da može imati parametre, što koristimo za inicijalizaciju našeg objekta. Nemojte da vas brine što imamo parametar name i svojstvo this.name - to su dve sasvim različite stvari. Ipak, nemojte da pravite lokalnu promenljivu (var name) sa istim imenom kao parametar - to bi dovelo do greške.
Objekat this
Hajde prvo da razjasnimo kako funkcioniše this kada dodajemo sa svojstva i metode. Obratite pažnju na sledeći isečak:
function Robot(name)
{
var prom = 1;
this.energy = 100;
this.charge = function() {
prom++;
this.energy++;
};
}
Možda biste pomislili da this.energy "funkcioniše" unutar funkcije, zato što je to metod, takođe vezan za this - this.charge. U stvari ova dva this objekta su dve različite stvari. this.charge je bitno samo pri kreiranju objekta. To služi da se objektu (za koji je pozvana funkcija Robot) "zalepi" svojstvo charge. Svojstvo charge je referenca na funkciju (anonimnu), pa efektivno funkcioniše kao metod objekta.
Unutar te funkcije, imamo drugi objekat this gde radimo sa njegovim svojstvom energy. Ovaj this ima vrednost tek kada se funkcija pozove, i predstavlja kontekst funkcije. Uobičajeno, ova dva this objekta predstavljaju istu stvar, ali pogledajte sledeći izuzetak:
var prvi = new Robot("Nešto");
var drugi = new Robot("Drugo");
prvi.charge.call(drugi);
Znamo da JavaScript omogućava da pri pozivu definišemo kontekst funkcije, odnosno objekat za koji se ona poziva, korišćenjem metoda call() i apply(). Ovde smo to primenili kako bismo pozvali metod charge() prvog objekta za drugi objekat. U tom slučaju, nastaje totalna zbrka - this unutar charge() je vezano za objekat drugi, ali unutrašnja promenljiva prom je iz objekta prvi, pošto je pri kreiranju objekta kreiran i closure. Proverite na samom primeru.
Privatne funkcije
Hajde da sada malo zakomplikujemo stvari. U "pravom životu" se često dešava da sami po sebi metodi budu suviše kompleksni ili da koriste jedan te isti deo programa koji se ponavlja, pa nam je lakše da njihovo izvršavanje razdelimo na funkcije. Kako ne bismo "prljali" ostatak programa, želećemo da te funkcije deklarišemo u "lokalu", tj. unutar funkcije klase. Ovo će biti privatne funkcije (a možemo isto tako dodati i promenljive), i biće sasvim dostupne metodima objekta.
Sve bi to bilo sasvim u redu, kada se ne bi desila sasvim uobičajena stvar - da nam u nekoj od tih privatnih funkcija zatreba da pozovemo neki metod ili da iskoristimo svojstvo objekta. Pokušaj da ovo uradimo prostim this.svojstvo dovešće do greške.
Pogledajte sada isečak u kome imamo privatnu funkciju, ali koja pristupa svojstvu objekta:
function Robot()
{
var _self = this;
var prom = 1;
this.energy = 100;
function fun() {
prom++; // u redu
this.energy++; // NEISPRAVNO!!!
_self.energy++; // ovako može
}
}
Problem je u tome što lokalna (privatna) funkcija, fun() ne može pristupati svojstvima i metodima objekta preko this. Ne zaboravite - this ne zavisi od deklaracije funkcije, već od njenog poziva!
Ovakav problem pristupa smo rešili tako što smo dodali novu promenljivu _self u koju na samom početku smeštamo objekat this. Pošto je ovo lokalna promenljiva, a ne svojstvo, unutrašnje funkcije joj pristupaju bez problema. Sa druge strane, pošto funkcioniše closure, promenljiva _self će zadržati referencu na objekat this iz vremena poziva funkcije Robot() (tj. konstrukcije objekta), pa će tako sve funkcije imati pristup objektu upravo preko promenljive _self.
Pogledajte kako sve to funkcioniše na "živom" primeru, gde smo primenili sve opisano.
Svojstvo constructor
Kada kreiramo objekat pomoću konstruktorske funkcije i operatora new, objekat implicitno dobija i jedno posebno svojstvo - constructor, koje sadrži referencu prema funkciji pomoću koje smo kreirali objekat.
function Klasa() {...}
var obj = new Klasa();
obj.constructor // referenca na funkciju Klasa
Na ovaj način možemo kreirati novi objekat pomoću iste funkcije klase, čak iako ne znamo koja je njegova konstruktorska funkcija.
var novi = new obj.constructor();
Operator instanceof
Vezano za konstruktor objekta, u JavaScriptu postoji poseban operator instanceof, pomoću koga možemo da ispitamo da li je određeni objekat nastao korišćenjem konkretne konstruktorske funkcije. Vrednost ove operacije je logičkog tipa.
function Klasa() {...}
var obj = new Klasa();
obj instanceof Klasa // vrednost true
Primetite da se funkcija navodi samo imenom, bez zagrada. Kada bismo napisali Klasa(), to bi značilo poziv funkcije.
Konstruktorska funkcija koja sama kreira objekat
Konstruktorsku funkciju je moguće formulisati i na drugi način:
function Klasa()
{
var promenljiva = vrednost;
function fun() {...}
return {
svojstvo: vrednost,
metod: function() {...}
};
}
Iako izgleda kao da je ova funkcija samo "prepakovana" verzija prethodne, razlika je suštinska. Funkcija ne očekuje da bude pozvana iz konteksta nekog objekta, već sama kreira objekat i vraća ga kao rezultat. Sve ostalo je isto - i ovde možemo imati lokalne promenljive i funkcije koje "glume" privatne metode.
Ovde smo "isforsirali" kreiranje objekta preko literala u direktivi return. Ako vam se čini malo nepregledno, skoro ista stvar je kada u funkciji prvo kreiramo objekat, onda mu dodajemo svojstva i tek na kraju ga vratimo kao rezultat:
function Klasa()
{
var promenljiva = vrednost;
function fun() {...}
var o = {};
o.svojstvo = vrednost;
o.metod = function() {...};
return o;
}
Pogledajte kako u ovom slučaju pravimo nov objekat:
var obj = Klasa();
Pošto funkcija sama kreira i vraća objekat kao rezultat, u ovom slučaju radimo bez operatora new. Pri svakom pozivu konstruktorske funkcije, kao rezultat dobijamo novu instancu (objekat).
Funkcija klase koja sama kreira objekat
Hajde da vidimo kako bi izgledao malopređašnji primer, ali u formi konstruktorske funkcije koja sama kreira objekat. Najprostiji slučaj, kada imamo samo svojstva i metode bi bio:
function Robot(name)
{
return {
name: name,
energy: 100,
charge: function() {
// punjenje robota
},
move: function(mX, mY) {
// pomeranje robota
}
};
}
var r = Robot("R2D2"); // KREIRANJE OBJEKTA
Dakle, sve je isto, samo malo drugačije. Ovde kreiramo objekat preko litarala u koji smo odmah ubacili sva četiri svojstva - ime i energiju (name i energy), kao i svojstva koja su reference na funkcije - metode charge i move.
Kreiranje objekta se takođe sasvim malo razlikuje od prethodne verzije. Vidimo da se ne koristi operator new, već da se samo poziva funkcija Robot() koja će nam vratiti kreirani objekat.
Kada se sve to završi, korišćenje samog objekta u programu se nimalo ne razlikuje od prethodne varijante.
// KORIŠĆENJE OBJEKTA
r.charge();
r.move(15, 20);
r.energy = 500;
Uvođenje lokalnih promenljivih i privatnih funkcija nije problem.
function Robot(name)
{
var potez = 0;
function provera() {...}
return {
name: name,
energy: 100,
charge: function() {
provera();
},
move: function(mX, mY) {
potez++;
}
};
}
Malo je zeznutija situacija ako želimo da privatne funkcije takođe pristupaju svojstvima objekta. Sada moramo da prepakujemo ceo konstruktor.
function Robot(name)
{
var potez = 0;
var _self = {
name: name,
energy: 100,
charge: function() {
plus();
},
move: function(mX, mY) {
potez++;
}
};
function plus() {
_self.energy++;
}
return _self;
}
Da bi closure funkcija plus() mogla da pristupi objektu, na njega mora da postoji konkretna referenca, ne možemo samo vratiti literal kao rezultat. Tako smo napravili referencu _self u kojoj smo definisali objekat, i koju na kraju vraćamo kao rezultat.
Naravno, ovde smo sami izabrali da se promenljiva zove _self (vodite računa, čisto "self" ima značenje u DOM programiranju, pa ga izbegavajte - zato smo mu i dodali donju crtu). Vi možete koristiti bilo koju promenljivu, s tim što pazite da je posle koristite za pristup objektu u svakoj privatnoj funkciji.
Pogledajte i primer...
Na pitanje koji je od ova dva načina bolji je teško dati odgovor. Kao i sa većinom tehnika, bolja je ona koja vam više odgovara, sa kojom bolje "plivate". Prvi način (sa operatorom new) podrazumeva malo šire korišćenu implementaciju prototipskog nasleđivanja, što bi kasnije moglo da vam znači. Sa druge strane, drugi način je malo "čistiji" i podrazumeva jasniji stil kreiranja prototipskog lanca. O svemu ovome će biti reči u tekstovima koji slede.
- S. Stefanov, K.C. Sharma (2013): Object-Oriented JavaScript, 2nd.ed, Packt Publishing, Birmingham
- Addy Osmani, Learning JavaScript Design Patterns