Klase u JavaScriptu
Suštinski, klase su uvedene u JavaScript da bi programi bili čitljiviji. Programeri koji su verzirani u objektno-orijentisanom programiranju, često smatraju da je JavaScript model rada sa objektima i nasleđivanjem, najblaže rečeno - nezgrapan. Zaista, rad sa funkcijama klase i prototipima objekata neće poboljšati čitljivost programa, i to je upravo problem koji klase rešavaju.
Ovo na žalost ne znači da JavaScript postaje "pravi" objektno-orijentisani jezik. Klase su samo sintaksni "ukras" (sytactic sugar), namenjen da poboljša čitljivost i olakša pisanje programa, ali suštinski se ništa ne menja.
JavaScript klase su dobra stvar za početnike u JavaScriptu, pa čak i za iskusne programere, pošto je zaista lako navići se na nov način pisanja. Sa druge strane, budite oprezni kada su u pitanju vaši korisnici, pošto su u vreme pisanja ovog teksta klase tek implementirane u web čitačima, pa je potrebno koristiti kvalitetniji (noviji) web browser.
Deklaracija klase
Klasa se deklariše upotrebom ključne reči class.
class Klasa {
...
}
Objekat klase kreiramo na već poznat način, korišćenjem operatora new.
var objekat = new Klasa();
Kada se kreira objekat, poziva se poseban metod, constructor() koji vrši inicijalizaciju objekta. U ovaj metod ubacujemo sve što je potrebno uraditi kada se objekat kreira. Posebno bitna stavka je dodavanje svojstava. Pošto je constructor() metod objekta, u njemu figurira objekat this koji predstavlja sam objekat i u koji onda možemo dodavati svojstva.
Metode dodajemo tako što ih navodimo kao funkcije, ali bez ključne reči function. Ovako dodati metodi odgovaraju metodima koji se dodaju u prototip, kada radimo na "stari" način, odnosno korišćenjem funkcija. Takođe, u svakom metodu, objekat this predstavlja sam objekat.
class Klasa {
constructor (parametri) {
this.svojstvo = početna_vrednost;
}
metod1 (parametri) {
}
metod2 (parametri) {
}
...
}
Mana ovog pristupa je što ne možemo uvesti "slobodne" promenljive koje nisu deo nekog metoda ili konstruktora. To znači da nema simuliranja "privatnih" svojstava, osim unutar konstruktora. Zaista, niko nam ne brani da i tu preko svojstava uvedemo funkcije koje će biti vezane za konkretan objekat preko closure mehanizma.
class Klasa {
constructor (parametri) {
var prom = vrednost;
this.metod = function(parametri) {};
}
...
}
Klase, kao i funkcije, mogu biti anonimne. Znači, moguće ih je navesti i bez konkretnog naziva, kao i praviti referencu na klasu.
var KlasaRef = class {
...
}
var NovaKlasa = class extends KlasaRef {
...
}
Takođe, klase je moguće i prosleđivati kao parametar ili vraćati kao vrednost funkcije. Funkcija koja prima klasu kao parametar, onda kreira novu klasu koja nasleđuje zadatu klasu i koju onda vraća bi izgledala ovako:
function kreator (klasa) {
return class extends klasa {
...
}
}
Takvu funkciju bismo mogli da upotrebimo da kreiramo novu nasleđenu klasu na osnovu nje ili čak da direktno definišemo objekat:
class xx extends kreator(class {...});
var obj = new xx();
...ili
var obj = new (kreator(class {...}))();
Getteri i setteri
Ukratko, getteri i setteri su funkcije koje služe za napredno definisanje svojstva. One praktično predstavljaju "prvu liniju odbrane" svojstva, tačnije sprečavaju programere da pristupaju svojstvima kao "golim" podacima.
Getter je funkcija koja obezbeđuje čitanje svojstva. Šta god da radi, na kraju mora pomoću direktive return da vrati vrednost koja onda predstavlja vrednost svojstva.
Slično, setter je funkcija koja se poziva kada se zadaje vrednost svojstva. Ova funkcija mora imati jedan parametar koji predstavlja zadatu vrednost.
Negde "u pozadini" se ta vrednost beleži - najčešće u nekom svojstvu objekta, koje se ne "eksponira". Naravno, JavaScript nema mehanizam kojim bi se svojstvo zaista sakrilo, tako da je u pitanju prosto konvencija - dogovor.
class Klasa {
constructor(params) {
this._skriveno_svojstvo = početna_vrednost;
}
get svojstvo() {
return this._skriveno_svojstvo;
}
set svojstvo(vrednost) {
this._skriveno_svojstvo = vrednost;
}
...
}
Kada se svojstvo definiše na ovakav način, koristimo ga kao i bilo koje "obično" svojstvo.
var objekat = new Klasa(params)
var citanje = objekat.svojstvo;
objekat.svojstvo = nova_vrednost;
Nasleđivanje
Jedna od najvažnijih "tekovina" OOP-a, je nasleđivanje, odnosno mogućnost da novu klasu baziramo na nekoj ranijoj klasi. Obično je ta "starija" klasa u nekom smislu "apstraktnija", predstavlja neka osnovna ponašanja objekta, dok je nova klasa "konkretnija" i uvodi nove mogućnosti i promene u starim ponašanjima.
Pomoću klasa u JavaScriptu, nasleđivanje je veoma pojednostavljeno. Dovoljno je koristiti ključnu reč extends u deklaraciji klase.
Takođe, pozivom specijalne funkcije super() iz konstruktora izvedene klase, pozivamo konstruktor osnovne klase.
class Osnovna {
constructor(osnovni_parametri) {
...
}
...
}
class Izvedena extends Osnovna {
constructor(novi_parametri) {
super(osnovni_parametri);
...
}
...
}
Treba da znamo da, ako koristimo funkciju super(), onda taj poziv konstruktora nadklase mora biti naveden u konstruktoru podklase pre korišćenja objekta this. Znači to je nešto što mora da se uradi pre no što počnemo npr. da ubacujemo nova svojstva u objekat. Naš savet je da u nasleđenoj klasi uvek započnete konstruktor pozivom funkcije super().
U izvedenim klasama obično kreiramo metode koji pokrivaju istoimene metode iz osnovne klase. Međutim, metodi iz osnovne klase često rade nešto što nam je potrebno i u nasleđenoj klasi, a što bismo morali da kopiramo u novom metodu.
U objektno-orijentisanom programiranju postoji način da iz podklase pozovemo metod nadklase, a isto to je moguće i kada koristimo klase u JavaScriptu, korišćenjem objekta super, koji nas povezuje sa osnovnom klasom.
class Osnovna {
constructor(...) {...}
metod(osnovni_parametri) {...}
...
}
class Izvedena extends Osnovna {
constructor(...) {
super(...);
...
}
metod(novi_parametri) {
...
super.metod(osnovni_parametri);
...
}
...
}
Naravno, nismo uslovljeni da metod nadklase pozivamo isključivo iz istoimenog metoda podklase. Preko objekta super, možemo pristupati bilo kom metodu nadklase, iz bilo kog metoda podklase.
Statički metodi
Isto kao u klasičnim objektno-orijentisanim jezicima, moguće je deklarisati i tzv. metode klase, odnosno statičke metode. To su metodi koji ne pripadaju objektu, već samoj klasi. Ne možemo ih koristiti za konkretan objekat, jer ne mogu pristupati njegovim svojstvima.
Statički metodi su prosto način da grupišemo metode pod nekim zajedničkim nazivom. Isti efekat bismo postigli i kreiranjem jednostrukog objekta, a sa dosadašnjim znanjem, jednostavno bismo funkciji koja predstavlja konstruktor nadodali nova svojstva koja su reference na statičke funkcije. Tipičan objekat sa statičkim metodima u JavaScriptu je Math.
Srećom, korišćenjem klasa u JavaScriptu i ovo je prilično olakšano:
class Klasa {
...
static metod(parametri) {...}
...
}
Ovakve metode onda pozivamo direktno iz klase, a ne iz objekta.
Klasa.metod(parametri);
Rad sa klasama
Ovde ćemo isprobati kako "rade" klase na primeru robota.
class Robot
{
constructor(ime) {
this.X = 0; // pozicija robota
this.Y = 0;
this.name = ime; // ime robota
this.energy = 100; // energija
}
charge() {
// ...punjenje baterije
}
move(mX, mY) {
// ...kretanje robota
}
}
Najpre smo kreirali osnovnu klasu - Robot. Svaki robot će imati poziciju (X i Y), energiju i ime. To će biti svojstva našeg objekta i njih inicijalizujemo unutar konstruktora. Da bi se kreirao robot, potrebno je samo proslediti ime robota, kao parametar.
Odmah posle konstruktora dodajemo, metode. Metod charge() služi za punjenje baterije, a move() za pomeranje robota.
Evo sada jednog jednostavnog nasleđivanja. Kreiramo klasu Battlebot kao izvedenu klasu klase Robot.
class Battlebot extends Robot
{
constructor(name, energy) {
super(name); // prvo pozivamo konstruktor za običnog Robota
this.energy = energy; // onda tek radimo sa svojstvima
this.MAX_ENERGY = 500; // maksimalna moguća energija za robota
this.ENERGY_CHUNK = 10; // kada se puni, za ovoliko se puni
}
charge() { // pokrivamo originalni charge() metod
if (this.energy < this.MAX_ENERGY) {
this.energy += this.ENERGY_CHUNK;
}
}
fire() { // novi metod fire() - to je specifično za borbenog robota
// ..."Destroy! Destroy!"
}
}
U Klasi Battlebot takođe imamo konstruktor constructor() koji sada ima dva parametra. Prvi je ime robota, a drugi je energija. E, sada, nema potrebe da ponavljamo sve što u inicijalizaciji objekta radi osnovna klasa. Dovoljno je da pozovemo funkciju super() sa parametrom koji zahteva osnovna klasa (prosleđujemo ime robota). Tek posle poziva ove funkcije, krećemo sa inicijalizacijom koja je bitna za "borbenog robota" (dodajemo energiju, maksimalnu energiju i za koliko se energija dopunjuje).
Klasa Battlebot uvodi metod charge() koji postoji i u osnovnoj klasi. Kada uradimo ovakvu stvar, novi metod "pokriva" metod iz osnovne klase. Takođe se uvodi sasvim novi metod fire() koji je specifičan samo za klasu Battlebot.
Kreiranje i korišćenje objekata je uvek isto.
// KREIRANJE OBJEKATA
var b = new Battlebot("Johnny5", 400);
// KORIŠĆENJE OBJEKATA
b.charge(); // koristi se novi metod charge umesto nasleđenog
b.move(15, 20); // nasleđeni metod za kretanje
b.fire(); // ovo može samo battlebot
Hajde sada da demonstriramo još neke stvari o kojima smo pisali. Napravićemo još jednu klasu Flybot, koja takođe nasleđuje osnovnog Robota.
class Flybot extends Robot
{
constructor(name) { // konstruktor
super(name); // poziv nasleđenog konstruktora (iz Robota)
this.alt = 0; // novo svojstvo
this._light = 5; // "tajno" svojstvo u kome beležimo vrednost
}
move(mX, mY, mAlt) { // nova verzija metoda move()
this.alt += mAlt; // menjamo visinu
super.move(mX, mY); // a ostatak kretanja odrađuje metod iz osnovne klase
}
get light() { // getter za svojstvo light
return this._light;
}
set light(val) { // setter za svojstvo light
val = (val < 0 ? 0 : val);
val = (val > 10 ? 10 : val);
this._light = val;
}
static default() { // statički metod - nema veze sa konkretnim objektom
return new Flybot("Flyer");
}
}
Recimo da je Flybot isti kao običan Robot samo što može da leti (i još osvetljava okolinu). Pošto leti, posle poziva konstruktora osnovne klase super(), dodajemo još i svojstvo alt koje označava visinu i "skriveno" svojstvo _light koje označava jačinu svetla.
Sada menjamo metod move() tako što dodajemo i treći parametar koji označava promenu visine robota. Unutar metoda smo definisali kako se postiže nova visina, a što se tiče pomeranja levo-desno i napred-nazad, to je nešto što već odrađuje običan Robot. Mi ne moramo da znamo na koji način i nije nas briga. Sve što treba da uradimo je da pozovemo metod move() osnovne klase, korišćenjem objekta super i da proseldimo ostatak parametara za kretanje.
Druga stvar kojom se bavimo je kreiranje gettera i settera za svojstvo light. Getter je jednostavan - to je (u ovom slučaju) prosto funkcija koja vraća vrednost nivoa osvetljenja (što je sačuvano u svojstvu _light).
Setter je sa druge strane malo složeniji. Želimo da, kada zadamo vrednost svojstva light, izvršimo proveru kako vrednost ne bi mogla da bude negativna ili veća od 10. Tu proveru vršimo u funkciji i tek onda beležimo novu vrednost.
Konačno, napravili smo i metod klase default(), koji nam služi da kreira novog "letača", sa nekim unapred zadatim podešavanjima.
Hajde da vidimo kako bi funkcionisao robot klase Flybot.
// KREIRANJE OBJEKATA
var fb = new Flybot("MyDrone");
// KORIŠĆENJE OBJEKATA
fb.charge(); // koristimo nasleđeni metod za punjenje baterije
fb.move(5, 30, 10); // novi metod za kretanje ima tri parametra
fb.light = 13; // zadajemo novu vrednost osvetljenja (poziva setter)
console.log(fb.light); // ispisuje vrednost 10 (poziva getter)
fb._light = 123; // moguće, ali izbegavamo to da radimo
// KORIŠĆENJE METODA KLASE
var ff = Flybot.default(); // pozivamo metod klase (statički metod)
Kao što vidimo, zahvaljujući setteru, svojstvo light je "zaštićeno" od neregularnih vrednosti - iako smo zadali vrednost 13, ona je svedena na 10, što je maksimum koji smo definisali. Sa druge strane, nedisciplinovane programere nikako ne možemo sprečiti da pristupaju svojstvu _light, iako to ne želimo.
Takođe smo iskoristili i metod klase Flybot.default() da odmah inicijalizujemo još jednog Flybota sa unapred zadatim vrednostima.
Višestruko nasleđivanje
JavaScript ne podržava višestruko nasleđivanje, odnosno mogućnost da jedna klasa ima više od jedne osnovne klase. Ipak ovakvo ponašanje bi se moglo donekle simulirati, korišćenjem funkcija koje kao rezultat vraćaju novu klasu koja nasleđuje zadatu klasu.
Recimo da nam je potrebno da omogućimo da svaki tip robota može biti i letač, kao i da može imati funkcionalnosti štita. Bilo bi nepraktično praviti podklase za baš svaki tip robota kome bismo želeli da dodamo ove funkcionalnosti. Višestruko nasleđivanje bi bilo rešenje za ovaj izazov, a u nedostatku istog, koristićemo tzv. mix-in funkcije.
function FlyMixin(osnovna) {
return class extends osnovna {
constructor(...args) {
super(...args)
//...
}
fly() {}
}
}
function ShieldMixin(osnovna) {
return class extends osnovna {
constructor(...args) {
super(...args)
//...
}
shieldOn() {}
shieldOff() {}
}
}
// KREIRANJE OBJEKATA
var m = new (FlyMixin( ShieldMixin(Robot) ))("Multipraktik");
// KORIŠĆENJE OBJEKATA
m.charge(); // koristimo nasleđeni metod za punjenje baterije
m.fly(); // ovaj robot ima i mogućnost letenja
m.shieldOn(); // kao i štit
Dakle, napravili smo dve funkcije, FlyMixin() i ShieldMixin() čija je uloga da kao parametar prihvate neku klasu, a onda kao rezultat vrate novu klasu koja proširuje zadatu klasu. Funkcija FlyMixin() zadatu klasu proširuje metodom fly(), dok ShieldMixin() zadatu klasu nasleđuje i proširuje sa dva metoda. Ovo će ovako funkcionisati za bilo koju klasu koju prosledimo kao parametar.
Ne dozvolite da vas zbuni kreiranje objekta. Kao osnovnu-osnovnu klasu zadajemo klasu Robot. Funkcija ShieldMixin() je proširuje i vraća novu klasu koju onda prosleđujemo funkciji FlyMixin() koja opet vrši proširivanje, dodaje svoj metod i ponovo vraća novu klasu.
Za tu novu klasu kreiramo objekat operatorom new i prosleđujemo parametar sa imenom robota - to je ono u poslednjoj zagradi. Obratite pažnju - poziv funkcije koja vraća klasu mora biti u zagradi, da bi prvo bila pozvana funkcija, a tek onda izvršen operator new.
Ovde primenjujemo još jednu novu stvar - tzv. spread operator "...", koji definiše neodređen broj parametara. Na ovaj način smo omogućili da se koliko god parametara zadamo, svi prosleđuju konstruktoru osnovne klase.
- Mozilla Developer Network, Classes