webdevlpr

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

  1. klasu deklarisemo sa kljucnom rijeci class i
  2. 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.

  1. Metode u klasi nastanjuju .prototype klase.
  2. Djecija klasa koja je prosirila (extended) roditeljsku klasu dobija DjecijaKlasa.prototype.[[Prototype]] = RoditeljskaKlasa.prototype.
  3. Instanca djecije klase dobija objekat.[[Prototype]] = DjecijaKlasa.prototype
  4. Sva polja postaju direktna svojstva objekta
  5. Ako zvuci zbunjujuce:
Macka.prototype[[Prototype]][[Prototype]]: Object__proto__constructor: Mackafraza: ƒ fraza(){}Munchkin.prototypenew Munchkin()[[Prototype]][[Prototype]]: Mackakaze: ’meow’prica: ƒ prica()constructor : Munchkin__proto__vrsta: ’Munchkin’ime : ‘Odie’{}oddie

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:

  1. Da bi razrijesili vrijednost dijete varijable, pozivamo new Dijete().
  2. konstruktor Dijete klase kreira konstruktor jer ga nismo naveli i poziva super konstruktor.
constructor(...args) {
  super(...args);
}
  1. U njemu se inicijalizuje prazan objekat {}.
  2. Potom polja postaju svojstva tog objekta. Objekat dobija { polje: 0.3 }.
  3. Izvrsava se konstruktor jer nema drugih polja. Konstruktor sa console.log ispisuje this.polje koje je trenutno svojstvo objekta 0.3
  4. Nakon izvrsenja konstruktora Roditelj klase, izvrsenje se vraca na Dijete klasu
  5. Polja u Dijete klasi se inicijalizuju i polje dobija novu vrijednost 1000;
  6. Nastavlja se izvrsenje konstruktora, ali u njemu nema vise instrukcija jer ga nismo kreirali i
  7. 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

  1. Polje postaje svojstvo instance
  2. Metoda postaje dio prototipnog lanca
  3. 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
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

[[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