Klase
U objektno-orijentisanom programiranju, klasa je prosiriv sablon programskog koda za kreiranje objekata. Uz kreiranje objekta, ona obezbjedjuje pocetne vrijednosti za stanja (clanske varijable) i implementacije ponasanja (clanske funkcije ili metode). Wikipedia
*JavaScript nema klase onako kako ih imaju neki drugi jezici. JavaScript ih imitira na osnovu prototipnog nasljedjivanja. Zato ohrabrujem razumijevanje prototipa i prototipnog nasljedjivanja prvo jer jer je klasa implementirana na ovom mehanizmu.
Klase su dodane sa uvodjenjem ECMAScript 6 (es6) kako bi, izmedju ostalog, JavaScript podrzavala objektno orjentisani stil programiranja i kako bi se izaslo u susret programerima sa pozadinom objektno orjentisanih jezika koji se baziraju na klasnom nasljedjivanju (poput: Java, C++, C#..).
Mozemo reci da je JavaScript skriptni programski jezik visestruke paradigme i podrzava objektno-orijentisani, imperativni i funkcionalni nacin programiranja. Paradigma se moze definisati kao pristup rjesavanju problema, a programska paradigma je nacin rjesavanja problema koristenjem programskog jezika.
Neki istaknuti clanovi JavaScript zajednice smatraju da je funkcionalni nacin programiranja ispravan i prirodan JavaScript-u, a da OOP i klasa kao "sintaticki secer" moze izazvati glavobolje. Jedan od argumenata je da funkcionalno programiranje osigurava jednostavniju kontrolu toka u kodu, lakse razumijevanje koda i izbjegava iznenadjenja u obliku promjena varijabli, stanja i pojavljivanja gresaka.
Diskusije o tome da li se klase trebaju koristiti u JavaScript-u se neprekidno vode. Medjutim, to nije razlog da slijepo prihvatimo slicne stavove i odbacimo klase bez razumijevanja. Ovaj odjeljak ne sluzi da osudi upotrebu klase, vec da ohrabri istrazivanje i poznavanje klase kako bi donosili svoje zakljucke.
Osnovno
Klasa je funkcija i sintaksa podsjeca na konstruktore jer se poziva sa kljucnom rijeci new
i naziv klase pocinje sa velikim slovom, ali se razlikuje po tome sto
- klasu deklarisemo sa kljucnom rijeci
class
i - ne navodimo zagrade za parametre kao sto to obicno radimo sa funkcijama.
Zato je u klasi dostupna constructor(x, y) {...}
metoda u koju mozemo smjestiti parametre. Automatski se izvrsava nakon pozivanja klase new Klasa(1, 2)
da bi inicijalizovala novi objekat. Ukoliko konstruktor metoda nije navedena, JavaScript ce dodati nama nevidljivu i praznu konstruktor metodu.
// let Klasa = class {} ili
// let Klasa = class ImenovanaKlasaIzraz {} ili
class Klasa {
constructor(id) {
this.id = id; // obj.id
this.metoda = function() {}; // obj.metoda
};
ispisiID() {
console.log(this.fraza + this.id);
}; // direktne metode => Klasa.prototype
fraza = 'Moj id je: ';
// direktna svojstva => obj.fraza
// u konstruktor funkciji bi definisali svojstvo
// this.fraza = ...
// Sa direktnim clanovima klase to ne radimo,
// samo sa clanovima unutar metoda
}
let obj = new Klasa(123);
obj.ispisiID(); // Moj id je: 123
console.log(obj);
// {fraza: 'Moj id je: ', id: 123, metoda: ƒ}
Klasa ima jednu specijalnu constructor
metodu, ostale metode se automatizovano dodaju u Klasa.prototype
. .prototype
klase nasljedjuju inicijalizovani objekti kao svoj [[Prototype]]
. Sa druge strane, svaka instanca klase dobija svojstva direktno u objektu, ne kroz prototip.
Hajde da vidimo obj
u DevTools konzoli (obratite paznju na to gdje se nalazi fraza
, a gdje ispisiID
metoda):
× Klasa fraza: 'Moj id je ' id: 123 metoda: ƒ metoda() × [[Prototype]]: Object > constructor: class Klasa > ispisiID: ƒ ispisiID() > [[Prototype]]: Object
Kao sto smo u clanku o prototipima naveli, svaka funkcija u svom prototip svojstvu ima constructor
svojstvo koje pokazuje na samu funkciju.
console.log(typeof Klasa); // function
console.log(obj.__proto__ === Klasa.prototype); // true
console.log(obj.constructor === Klasa); // true
let obj1 = new obj.constructor(321);
console.log(obj1); // {fraza: 'Moj id je: ', id: 321}
Isto mozemo uraditi bez klase, koristeci konstruktor i prototipe.
function Konstruktor(param) {
this.id = param;
this.fraza = 'Moj id je: ';
}
Konstruktor.prototype.ispisiID = function() {
console.log(this.fraza + ' ' + this.id);
};
let obj = new Konstruktor(123);
// obj = {fraza: 'Moj id je: ', id: 123}
obj.ispisiID(); // Moj id je: 123
Petlje ne vide metode
Petlje ne vide metode u prototipu klase jer ova svojstva za enumerable
deskriptor dobijaju false
vrijednost, dok to sa konstruktorom u drugom primjeru nije slucaj.
// class Klasa {...}
let deskriptori = Object.getOwnPropertyDescriptors(Klasa.prototype);
console.log(deskriptori);
Daje:
{ constructor: { writable: true, enumerable: false, configurable: true, value: class Klasa }, ispisiSvojstvo: { writable: true, enumerable: false, configurable: true, value: ƒ ispisiSvojstvo() } }
Mozemo napisati petlju koja ispisuje svojstva objekta kreiranog konstruktor funkcijom, ne klasom. Njegova metoda ispisiID()
u prototipu ima vrijednost enumerable: true
i zato je je petlja vidi.
for (let svojstvo in obj) {
console.log(svojstvo); // id, fraza, ispisiID
}
use strict
se podrazumijeva unutar klase. Svi dijelovi tijela klase su u striktnom rezimu. Bez striktnog rezima, ovo bi radilo:
class Klasa {
constructor() {
varijabla = ''; // varijabla is not defined
console.log(varijabla);
}
}
let obj = new Klasa();
Izgradnja strukture sa geterima i seterima
Pustanje korisnika klase da radi sa svojstvima direktno se potencijalno smatra losim dizajnom. Mozemo zastititi i zamaskirati stvarna svojstva setterima i getterima koji ce imati pristup pravim svojstvima objekta. Zasticena svojstva obicno imaju _
prefix.
class Konstruktor {
constructor(fraza) {
this.fraza = fraza; // poziva set fraza('Dobar dan.')
}
set fraza(vrijednost) {
this._fraza = vrijednost; // dodjeljuje _fraza: 'Dobar dan.'
}
get fraza() {
return this._fraza;
}
}
let obj = new Konstruktor('Dobar dan.');
console.log(obj); // {_fraza: 'Dobar dan.'}
obj.fraza = 'Dobro vece.';
// poziva set fraza('Dobro vece.')
console.log(obj.fraza);
// poziva get fraza() { return this._fraza }
// Dobro vece
Pozivanjem obj.fraza
svojstva, pozivaju se setter ili getter ako su definisani (u ovisnosti sta zelimo uraditi).
Potencijal ovog koda je vise od definisanja neke vrijednosti ili vracanja iste. Metode se ponasaju kao svojstva sto nam daje moc da kontrolisemo podatke: svojstvima dodamo logiku, provjere prije nego svojstvo prihvati vrijednost ili instrukcije za vracanje vrijednosti u specificnom obliku.
Razlog za prefix "_
" je razlikovanje izmedju setera / getera i originalnog svojstva. Po konvenciji, izvan klase ne treba manipulisati sa svojstvima sa prefixom "_
" iako je moguce. Drugi razlog je da ako JavaScript engine ne moze razlikovati setter/getter metodu od svojstva dolazi do RangeError: Maximum call stack size exceeded
.
Kako? Hajde da originalno svojstvo preimenujemo u ime settera.
constructor(fraza) {
this.fraza = fraza;
// this.fraza poziva set fraza('Dobar dan.')
}
...
set fraza(vrijednost) {
this.fraza = vrijednost;
// this.fraza beskonacno poziva set fraza('Dobar dan.')
}
Sa getterima mozemo dobiti Maximum call stack size exceeded
na isti nacin.
Da bi se svojstvo moglo samo citati, ali ne i mijenjati dovoljno je definisati getter bez settera.
Ispod setter dobija instrukcije da provjeri broj rijeci u proslijedjenoj frazi. Ukoliko postoji vise od jedne rijeci vrijednost je prihvacena, inace porukom sugerise korisniku da pokusa frazu sa vise rijeci.
class Konstruktor {
constructor(fraza) {
this.fraza = fraza;
}
set fraza(vrijednost) {
let nizRijeci = vrijednost.split(' ');
if (nizRijeci.length > 1) {
this._fraza = vrijednost;
console.log(`Nova fraza je "${this._fraza}"`);
} else {
console.log(`Pokusajte frazu sa najmanje dvije rijeci.`);
}
}
get fraza() {
return this._fraza;
}
}
let obj = new Konstruktor('x');
// Pokusajte frazu sa najmanje dvije rijeci
console.log(obj);
// {}
obj.fraza = 'Dobro vece.';
// Nova fraza je "Dobro vece."
console.log(obj);
// { _fraza: 'Dobro vece.' };
Prosirivanje klase i nasljedjivanje
Da bi prosirili klasu, odnosno da bi jedna naslijedila od druge koristimo kljucnu rijec extends
.
class Macka {
kaze = 'meow';
fraza() {
console.log(`${this.ime} kaze ${this.kaze}!`);
}
constructor(ime, vrsta, zvuk) {
this.ime = ime;
this.vrsta = vrsta;
if (zvuk) this.kaze = zvuk;
}
}
// definisanje nove klase sa extends
class Munchkin extends Macka {
prica() {
this.fraza();
}
}
let oddie = new Munchkin('Oddie', 'Munchkin');
oddie.prica(); // Oddie kaze meow!
console.log(oddie); // {kaze: 'meow', ime: 'Oddie', vrsta: 'Munchkin'}
Kljucna rijec extends
je "ukrala" mehanizam prototipnog nasljedjivanja.
- Metode u klasi nastanjuju
.prototype
klase. - Djecija klasa koja je prosirila (
extended
) roditeljsku klasu dobijaDjecijaKlasa.prototype.[[Prototype]] = RoditeljskaKlasa.prototype
. - Instanca djecije klase dobija
objekat.[[Prototype]] = DjecijaKlasa.prototype
- Sva polja postaju direktna svojstva objekta
- Ako zvuci zbunjujuce:
Ili ispis conosle.log(oddie)
u konzoli. Vidimo desnu stranu [[Prototype]]
nasljedjivanja jer pocinje od objekta oddie
:
× Munchkin vrsta: 'Munchkin' ime: 'Oddie' kaze: 'meow' × [[Prototype]]: Macka > constructor: class Munchkin > prica: ƒ prica() × [[Prototype]]: Object > constructor: class Macka > fraza: ƒ fraza() × [[Prototype]]: Object > constructor: ƒ Object > hasOwnProperty: ƒ hasOwnProperty() > isPrototypeOf: ƒ isPrototypeOf() ...
Kao u prototipima, JavaScript trazi metode uz lanac prototipnog nasljedjivanja.
Staticna polja i metode
Sa kljucnom rijeci static
mozemo dodati korisne funkcije klasi. Sigurno ste vidjali neke predefinisane staticne metode poput Math.max()
ili Object.getPrototypeOf()
.
Staticna polja i metode se dodjeljuju direktno klasi i dostupna su na njoj: Klasa.metoda = fn()
, sto se znacajno razlikuje od Klasa.prototype.metoda = fn()
cime smo se bavili do sada. Koriste se za kreiranje nekog "alata" koji nije specifican za izvedeni objekat i nisu dostupne u pojedinacim inicijalizovanim objektima kako ih ne nasljedjuju u prototipu.
class Student {
constructor(ime, smijer) {
this.ime = ime;
this.smijer = smijer;
}
static studiraEkonomiju(student) {
return (student.smijer === 'Ekonomija');
}
}
let ena = new Student('Ena', 'Ekonomija');
Student.studiraEkonomiju(ena); // true
Prosirenjem klase, djecija klasa nasljedjuje roditeljsku za svoj prototip. Tako djecija klasa nasljedjuje roditeljske staticne metode.Alumni.[[Prototype]] = Student
.
Class Student {
constructor(ime, smijer) {
this.ime = ime;
this.smijer = smijer;
}
static studiraEkonomiju(student) {
return (student.smijer == 'Ekonomija');
}
}
class Alumni extends Student {}
let ena = new Student('Ena', 'Ekonomija');
Alumni.studiraEkonomiju(ena); // true
Super
Kljucna rijec super
je referenca na roditeljski objekat ili klasu (super klasu) i sluzi za pristup njihovim metodama i poljima kroz [[Prototype]]
.
super.metoda(); // poziva roditeljsku istoimenu metodu
super.svojstvo; // poziva roditeljsko istoimenu svojstvo
super(...args); // poziva konstruktora roditeljske klase
Ono u cemu je super
dobar je ponovna upotreba vec napisanog koda, sa opcijom da ga prosiri tamo gdje ima potrebe kao sto to rade klase. Evo i kako:
Konstruktor
Kada zelimo iz konstruktora djecije klase pozvati roditeljsku pozivamo super(...args)
u obliku funkcije.
class Osoba {
constructor(ime, godine) {
this.ime = ime;
this.godine = godine;
}
}
class Student extends Osoba {
// Kada djecija klasa nema definisan konstruktor onda se generise:
// constructor(...args) {
// super(...args);
//}
}
let student = new Student('Tea', 23);
// student = { ime: 'Tea', godine: 23};
U primjeru iznad djecija klasa automatski poziva roditeljski konstruktor i inicijalizuje objekat student
. A ako zelimo definisati dodatne funkcionalnosti u konstruktoru djecije klase, onda konstruktora roditeljske klase moramo sami pozvati:
class Osoba {
constructor(ime, godine) {
this.ime = ime;
this.godine = godine;
}
}
class Student extends Osoba {
constructor(ime, godine, smijer) {
super(ime, godine);
this.smijer = smijer;
}
}
let student = new Student('Tea', 23, 'IT');
// student = { ime: 'Tea', godine: 23, smijer: 'IT' };
Trik je u tome da u konstruktoru djecije klase prije koristenja kljucne rijeci this
moramo pozvati roditeljski konstruktor, inace dolazi do greske:
ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
Radi se o tome da kljucna rijec new
inicijalizuje objekat ali to ne moze uraditi izvedena klasa, samo ona koja nema roditelja (protoParent: null
). Izvedene klase imaju interno svojstvo [[ConstructorKind]]: "derived"
. Tek kada super konstruktor inicijalizuje objekat, this
pokazuje na objekat. Prije toga this
nece raditi.
Metode
Kada djecija klasa ima trazenu metodu, onda JavaScript engine ne trazi dalje. Medjutim, ako zelimo dodati funkcionalnosti i prilagoditi metodu koja vec postoji u roditeljskoj klasi, ne moramo prepisivati vec napisan kod. U djecijoj klasi cemo kreirati istoimenu metodu i unutra pozvati super.metoda()
. Prije ili nakon poziva super metode mozemo dodati jos funkcionalnosti.
class Osoba {
ime = 'Tea';
godine = 23;
ispisInfo() {
return `Ime: ${this.ime}. Godina: ${this.godine}.`;
}
}
class Student extends Osoba {
smijer = 'informatika';
ispisInfo() {
return `${super.ispisInfo()} Smijer: ${this.smijer}.`;
}
}
let student = new Student();
console.log(student.ispisInfo());
// Ime: Tea. Godina: 23. Smijer: informatika.
Polja klase
Polja klase su svojstva instance, ali super
pokusava pristupiti svojstvima u .prototype
objektu superklase. Odnosno u [[Prototype]]
lancu instance. U primjeru ispod vrijednost polja nije nadjena u prototipu instance i zato je undefined
.
class RoditeljskaKlasa {
roditeljskoPolje = 15;
}
class DjecijaKlasa extends RoditeljskaKlasa {
djecijePolje = super.roditeljskoPolje; // undefined
}
let djecija = new DjecijaKlasa();
console.log(djecija); // {roditeljskoPolje: 15, djecijePolje: undefined}
Nadjacavanje polja klase
Kada su u pitanju dupla polja (polje istog imena u djecijoj i roditeljskoj klasi) postoji mala zackoljica zbog reda inicijalizacije polja klase i izvrsenja koda, koja moze izazvati nepredvidiv ishod. Sa druge strane, da su u pitanju metode rezultat koda bi bio "ispravan".
class Roditelj {
polje = 0.3;
constructor() {
console.log(this.polje);
}
}
class Dijete extends Roditelj {
polje = 1000;
}
let dijete = new Dijete(); // 0.3
console.log(dijete); // { polje: 1000 }
Pozivom console.log()
u super konstruktoru se ispisuje this.polje
prije nego je djecija klasa uspjela dodijeliti svoje polje
instanci.
Umjesto poziva this.polje
prije nego klasa zavrsi sa kreiranjem objekta, ispisacemo vrijednost polja kroz metodu:
class Roditelj {
polje = 0.3;
constructor() {
// console.log(this.polje);
}
vrijednostPolja() {
console.log(this.polje);
}
}
class Dijete extends Roditelj {
polje = 1000;
}
let dijete = new Dijete();
dijete.vrijednostPolja(); 1000
Sledeci odjeljak detaljnije ulazi u izvrsavanje koda i zasto se polja ponasaju drugacije:
- Da bi razrijesili vrijednost
dijete
varijable, pozivamonew Dijete()
. - konstruktor
Dijete
klase kreira konstruktor jer ga nismo naveli i poziva super konstruktor.
constructor(...args) { super(...args); }
- U njemu se inicijalizuje prazan objekat
{}
. - Potom polja postaju svojstva tog objekta. Objekat dobija
{ polje: 0.3 }
. - Izvrsava se konstruktor jer nema drugih polja. Konstruktor sa
console.log
ispisujethis.polje
koje je trenutno svojstvo objekta0.3
- Nakon izvrsenja konstruktora
Roditelj
klase, izvrsenje se vraca naDijete
klasu - Polja u
Dijete
klasi se inicijalizuju ipolje
dobija novu vrijednost1000
; - Nastavlja se izvrsenje konstruktora, ali u njemu nema vise instrukcija jer ga nismo kreirali i
- Varijabli
dijete
se dodjeljuje novi objekat{ polje: 1000 }
Zasto metode klase, za razliku od polja, rade ispravno?
Odmah po inicijalizaciji objekta, on za svoj [[Prototype]]
nasljedjuje Dijete.prototype
i pristup svim metodama u djecijoj i roditeljskoj klasi. Kako polja postaju svojstva instance pojedinacno, ona se inicijalizuju tek kada se kreira nova instanca u super klasi. Smijer inicijalizacije je Roditelj → Dijete.
static
svojstva
- Polje postaje svojstvo instance
- Metoda postaje dio prototipnog lanca
- Staticna metoda je direktno svojstvo klase
class Klasa {
ime = 'Tea'; // objekat.ime = Tea
static godine = 23; // Klasa.godine = 23
ispisInfo() { // Klasa.prototype.ispisInfo = function()
return `Ime: ${this.ime}. Godine ${this.godine}`;
}
}
let objekat = new Klasa();
console.log(objekat.ispisInfo()); // Ime: Tea. Godine: undefined.
console.dir(Klasa.godine); // 23
Sada kada imamo osnovno znanje o metodama i poljima mozemo se vratiti kljucnoj rijeci super
i osvrnuti kako ona zapravo radi na static
poljima. Ispod vidimo djeciju klasu koja prosiruje roditeljsku. Obe imaju static
polje i kao takvo postaje svojstvo klase. Klase nasljedjuju jedna drugu a super
trazi svojstvo u [[Prototype]]
tako da:
class RoditeljskaKlasa {
static rPolje = 100; // RoditeljskaKlasa.rPolje = 100;
}
class DjecijaKlasa extends RoditeljskaKlasa {
static dPolje = super.rPolje + 1; // DjecijaKlasa.dPolje = 100 + 1;
}
let djecija = new DjecijaKlasa();
console.log(djecija); // {}
// instanca je prazna
console.dir(DjecijaKlasa);
Poslednja linija u DevTools konzoli ispisuje sledece:
× class DjecijaKlasa (**) dPolje: 101 (**) length: 0 name: 'DjecijaKlasa' > prototype: 'RoditeljskaKlasa' arguments: (...) caller: (...) × [[Prototype]]: class RoditeljskaKlasa (**) rPolje: 100 (**) length: 0 name: "RoditeljskaKlasa" > prototype: {constructor: ƒ} ...
super
u objektu
super
ide uz prototipni lanac nasljedjivanja sve dok ne nadje super objekat koji ima trazeno svojstvo.
let osoba = {
ime: 'Jana Doe',
sePredstavlja() {
console.log(`Ja sam ${this.ime}.`);
}
};
let student = {
__proto__: osoba
};
let senior = {
__proto__: student,
ime: 'Teo',
sePredstavlja() { // [[HomeObject]] = senior
super.sePredstavlja();
}
};
senior.sePredstavlja(); // Ja sam Teo
- Kada u metodi koristimo kljucnu rijec
super
, ona veze metodu sa objekatom u kom se nalazi koristeci posebno[[HomeObject]]
svojstvo funkcije. - To svojstvo imaju samo funkcije stilizovane kao metode
metoda() {...}
. [[HomeObject]]
pamti objekat u kom je funkcija originalno definisana isuper
ovu vezu koristi za razrjesavanje metoda i roditeljskih prototipa.
let objekat = {
metoda () {
// objekat.metoda.[[HomeObject]] = objekat;
}
};
class Klasa {
metoda () {
// Klasa.prototype.metoda.[[HomeObject]] = Klasa.prototype;
}
}
function funkcija() {
// funkcija.[[HomeObject]] = undefined;
}
Kada je super
metoda pozvana, JavaScript engine trazi super
metodu u [[Prototype]]
-u [[HomeObject]]
-a. Ovo osigurava da je nadjena metoda naslijedjena, a ne dio trenutnog objekta.
class RKlasa {
metoda() {
console.log('Zdravo iz roditeljske metode');
}
}
class DKlasa extends RKlasa {
metoda () {
// jer metoda() postaje metoda DKlasa.prototype objekta
// DKlasa.prototype.metoda.[[HomeObject]] = DKlasa.prototype
super.metoda();
// super trazi .metoda() u DKlasa.prototype.[[Prototype]] lancu
console.log('Zdravo iz djecije metode');
}
}
new DKlasa().metoda();
// Zdravo iz roditeljske metode
// Zdravo iz djecije metode
console.dir(DKlasa);
Hajde da vidimo u konzoli gdje je pronasao super.metoda()
:
× class DKlasa length: 0 name: 'DKlasa' × prototype: (**) > constructor: class DKlasa > metoda: ƒ metoda() (*) × [[Prototype]]: (***) > constructor: class RKlasa` > metoda: ƒ metoda() > [[Prototype]]: Object arguments: (...) caller: (...) > [[Prototype]]: class RKlasa
Na liniji sa * je oznacena metoda u kojoj smo pozvali super.metoda()
Na liniji sa ** je objekat u kojoj je ta metoda kreirana [[HomeObject]]
Na liniji sa *** se nalazi [[Prototype]]
gdje pocinje potraga za super metodom
[[HomeObject]]
bi u obliku objekta za navedeni primjer mogli predstaviti ovako:
[[HomeObject]] = DKlasa.protototype: {
constructor: class DKlasa,
metoda: ƒunction() {
super.metoda();
console.log('Zdravo iz djecije metode')
},
[[Prototype]]: {
constructor: class RKlasa,
metoda: ƒunction() {
console.log('Zdravo iz roditeljske metode')
},
[[Prototype]]: {...}
}
}
Privatna polja i metode
Do sada su klase bile iskljucivo javne u JavaScript-u, za razliku od nekih drugih jezika. ES2020 uvodi privatne clanove (polja i metode) klase. A da bi neki clan bio privatan koristimo #
prefiks i ovakvi clanovi su dostupna samo iz klase. Ne mogu biti nasljedjeni.
class Osoba {
#ime;
#prezime;
constructor(ime, prezime) {
this.#ime = Osoba.#validiraj(ime);
this.#prezime = Osoba.#validiraj(prezime);
}
get punoIme() {
return this.#ime + ' ' + this.#prezime;
}
static #validiraj(str) {
if (typeof str === 'string' && str.length > 2) {
return str;
}
throw 'Vrijedno mora biti string, sa vise od 2 karaktera';
}
}
let osoba = new Osoba('Marta', 'Martinovic');
console.log(osoba.punoIme); // Marta Martinovic
class Student extends Osoba {
constructor(ime, prezime, smijer) {
super(ime, prezime);
this.smijer = smijer;
}
}
let student = new Student('Lepa', 'Cvetic', 'CS');
// student = { smijer: 'CS', #ime: 'Lepa', #prezime: 'Cvetic' };
let ime = Osoba.#validiraj('Anja');
// SyntaxError:
// Private field '#validiraj' must be declared in an enclosing class
U primjeru iznad smo inicijalizovali polja #ime
i #prezime
, kao i staticnu metodu #validiraj
. Navedeni clanovi su dostupni samo iz klase u kojoj su definisani. Za postavljanje vrijednosti i citanje privatnih clanova izvan klase pristupljeno je javnim metodama koje to rade iznutra.
Privatno i javno svojstvo istog imena ne stvaraju konflikt.
Privatna svojstva sa WeakMap
WeakMap
je kolekcija (kljuc/vrijednost) parova ciji kljuc mora biti objekat, dok vrijednost moze biti proizvoljnog tipa (sto se razlikuje od kolekcije Map
koja za kljuc moze imati bilo koju vrijednost).
WakMap
svojstva takodje ne stvaraju jake reference za svoje kljuceve, sto znaci da ako se objekat koji se koristio za kljuc izbrise, njegova vrijednost ce takodje biti uklonjena iz memorije i mape.
Da bi WeakMap
sakrila svojstvo u ovom primjeru cemo koristiti i modul, odnosno odvojicemo kod. Klasa i privatna varijabla ce imati svoj modul i iz njega cemo eksportovati samo klasu, ne i privatnu varijablu.
// stack.js
const _items = new WeakMap();
export class Stack {
constructor(stack = []) {
_items.set(this, stack);
}
peek() {
return _items.get(this)[this.count - 1];
}
pop() {
if (this.count < 1) throw Error('Invalid request, empty stack.');
return _items.get(this).pop();
}
push(value) {
return _items.get(this).push(value);
}
get count() {
return _items.get(this).length;
}
}
// app.js
import {Stack} from './stack.js';
const stack = new Stack();
Nemojte zaboraviti u HTML fajlu skripti dodati type="module"
atribut kako bi pretrazivac znao kako tretirati fajl (ili podesite webpack) i pokrenite stranicu kroz server. Ovdje je koristena sintaksa ES6 modula za pretrazivace.
CommonJS Module format za Node je nesto drugaciji:
// stack.js
module.exports = Stack;
// app.js
const Stack = require('./stack');
Prosirivanje ugradjenih konstruktora
Nekada zelimo prosiriti mogucnosti ugradjenih konstruktora poput Object
, Array
, Map
i drugih. Recimo da zelimo prosiriti Array
sa klasom za brojeve koja dodaje funkcionalnost poduplavanja i provjeravanja da li je trenutna vrijednost jednaka izvornoj.
class NizBrojeva extends Array {
constructor(...args) {
super(...args);
let kopijaNiza = {orig: [].concat(this)};
Object.assign(Object.getPrototypeOf(this), kopijaNiza);
}
dupliraj() {
return this.forEach((broj, index) => this[index] = broj * 2);
}
static jeIzvorni(niz) {
let izvorniNiz = Object.getPrototypeOf(niz).orig;
return JSON.stringify(niz) === JSON.stringify(izvorniNiz);
}
}
let niz = new NizBrojeva(1, 2, 4);
niz.dupliraj(); // [2, 4, 8]
console.log(NizBrojeva.jeIzvorni(niz)); // false
Provjera tipa
Ali kako mozemo odrediti tip klase NizBrojeva
koji smo vidjeli iznad?
console.log(niz instanceof NizBrojeva); // true
console.log({}.toString.call(niz)); // [object Array]
console.log(typeof niz); // object
instanceof
instanceof
operator radi na objektima i testira da li se bilo gdje u prototipnom lancu objekta nalaze svojstva .prototype
objekta navedenog konstruktora. Vraca true
ili false
. Sintakasa je objekat instanceof Konstruktor
.
Kako je istina da je niz
instanca klase NizBrojeva
, tako je istina da je i instanca Array
konstruktora jer se njegova prototip svojstva takodje nalaze u lancu prototipa objekta niz
. Na kraju je i instanca Object
konstruktora.
console.log(niz instanceof Object); // true
Kako je navedeno, instanceof
provjerava da li se svojstva prototip objekta konstruktora nalaze u prototipnom lancu instance, ali mozemo modifikovati logiku, odnosno navesti uslove koje objekat mora ispuniti da bi se smatrao instancom nekog konstruktora.
Logiku pisemo u staticnoj metodi konstruktora static [Symbol.hasInstance](objekat)
koja mora vratiti true
ili false
.
class NizBrojeva extends Array {
static [Symbol.hasInstance](instanca) {
return Array.isArray(instanca);
}
}
let niz = new Array();
console.log(niz instanceof NizBrojeva); // true
Znamo da objekat niz
nije naslijedio prototipna svojstva klase NizBrojeva
, ali nasa logika kaze da su svi nizovi instanca klase NizBrojeva
.
toString metoda
Object
u svom prototipu ima metodu .toString()
. Njegovo podrazumijevano ponasanje na objektu daje [object Object]
.
let objekat = {};
console.log(objekat.toString()); // [object Object]
Super moc ovakvog ponasanja nije ocigledna, ali mozemo posuditi Object.prototype.toString
metodu i primjeniti na drugim podacima kako bi dobili njihov tip.
{}.toString.call( 1234 ); // [object Number]
{}.toString.call( 'ab' ); // [object String]
{}.toString.call( 111n ); // [object BigInt]
{}.toString.call( [] ); // [object Array]
{}.toString.call( null ); // [object Null]
{}.toString.call( undefined ); // [object Undefined]
{}.toString.call( new Map() ); // [object Map]
{}.toString.call( new Set() ); // [object Set]
{}.toString.call( class {} ); // [object Function]
{}.toString.call( function() {} ); // [object Function]
Zakljucicemo da je {}.toString
naprednija verzija typeof
operatora. On radi na vecini primitivnih tipova podataka, ali null
prepoznaje kao objekat. Dok vecinu tipova objekata prepoznaje samo kao objekte, ne i kao njegov tip.
{}.toString
metodu smo posudili iz prototipa objekta i da bi je izvrsili u novom kontekstu (kontekstu podatka koji prosljedjujemo) koristimo .call()
. Razlog posudjivanja na ovaj nacin je jer drugi tipovi takodje imaju toString
metodu u svom prototipu, ali one nemaju ovakvo ponasanje, a nalaze se blize u prototipnom lancu nasljedjivanja.
Instance klasa imaju tip koji nasljedjuju od predefinisanih konstruktora poput [object Object]
ili [object Array]
, ali ako zelimo da {}.toString
metoda razlikuje instance nasih klasa dodacemo im [Symbol.toStringTag]: 'Tip'
u svojstvo.
class NizBrojeva {
[Symbol.toStringTag] = 'NizBrojeva';
}
let obj = new NizBrojeva();
console.log( {}.toString.call(obj) ); // [object NizBrojeva]
Mix-in
U OOP, mixin je klasa sa metodama koje druge klase mogu koristiti bez potrebe da od nje nasljedjuju. Sa ovim mozemo prevazici limitaciju nasljedjivanja u kojoj objekat moze imati samo jedan prototip i klasa moze prosiriti samo jednu klasu.
Mixini, kao i klase, ohrabruju koristenje vec napisanog koda umjesto dupliciranja (ali sa njima izbjegavamo i potencijalno nepredvidivo nasljedjivanje).
let mixIn = {
sePredstavlja() {
console.log(`Ja sam ${this.ime}.`);
}
};
class Student {
constructor(ime) {
this.ime = ime;
}
}
Object.assign(Student.prototype, mixIn);
let student = new Student('Ivana');
student.sePredstavlja(); // Ja sam Ivana.
Klasi smo dodali nove metode koje ce naslijediti njene instance, a Student
i dalje ima priliku da prosiri jednu klasu.
Hajde da ispisemo Student
klasu u konzoli i vidimo njena svojstva.
× Student length: 1 name: 'Student' × prototype: > sePredstavlja: ƒ sePredstavlja() // mixIn > constructor: class Student > [[Prototype]]: Object arguments: (...) caller: (...) > [[Prototype]]: ƒ () ...
Object.assign
metoda je "tajni" sastojak koji je iskoristen za mixin. Ona je kopirala svojstva objekta u drugom argumentu (source) i dodala u objekat koji smo ponudili kao prvi argument (target).
Sintaksa je Object.assign(target, ...sources)
.
Ova metoda ne radi "deep cloning". Ako je vrijednost svojstva referenca na objekat, to svojstvo se ne klonira vec se kopira referenca na objekat. Vise o prosljedjivanju po referenci na Proslijediti po referenci ili vrijednosti