JavaScript ES6 sintaksa
Uvod
Svako ko se bavi programiranjem bar izvesno vreme, svestan je velike popularnosti koju programski jezik JavaScript uživa duži niz godina unazad, a takođe verujemo da među čitaocima (koje zanima ovakav članak), postoji i veći broj pojedinaca koji razmišljaju o karijeri koja je vezana za neko od ((veoma) brojnih) radnih okruženja koja se zasnivaju upravo na JavaScript-u (jednom od nekoliko najpopularnijih jezika današnjice).
JavaScript je svoj "životni put" započeo kao skriptni jezik za pristup DOM elementima u web browserima (naravno, i danas obavlja navedenu ulogu), ali, tokom vremena, prevazišao je početne okvire i postao osnova velikog broja zanimljivih radnih okruženja (Node.JS, Express, React, Vue, Angular ....), koja zajedno čine svojevrstan "ekosistem" za razvoj, pre svega web aplikacija, ali takođe i desktop programa, kao i raznovrsnih biblioteka i dodataka.
Kada je u pitanju "životni put programera", koji su prethodno: razmotrili i ostale opcije (kojih takođe ima 'podosta'), izabrali JavaScript, savladali osnove jezika i žele da se usmere (u bliskoj budućnosti) na neku od ranije navedenih tehnologija (ili na neko od okruženja koje nismo pomenuli), postavlja se pitanje da li postoje tehnike koje (obavezno) treba savladati - što bolje - pre preduzimanja daljih, 'konkretnijih' koraka?
Smatramo da je odgovor (veoma nedvosmisleno) potvrdan * i smatramo da su tehnike koje treba savladati (posle osnova i pre upoznavanja sa nekim od pomenutih radnih okruženja) - upravo unapređenja koja su došla sa revizijom jezika ECMAScript 2015 (odnosno ECMAScript 6, ili još jednostavnije - ES6), za koja ćemo u daljem tekstu koristiti opšteprihvaćeni naziv - 'ES6 sintaksa'.
ES6 sintaksa - najvažniji elementi
Nećemo nabrajati sva unapređenja koja su došla sa revizijom ES6 (od kada popularnost JS-a zapravo beleži najveći porast) i nećemo previše ulaziti u detalje, ali, napravićemo osvrt na najznačajnije tehnike, koje - iz perspektive pojedinaca koji žele da se ozbiljnije bave Node.js aplikacijama ili da razvijaju sajtove u nekom od popularnih "framework-a" (kao što su React, Angular ili Vue) - slobodno možemo smatrati krajnje neophodnim.
Shodno prethodnim smernicama, ovaj članak biće svojevrstan informator za sve programere koji imaju nameru da se podrobnije upoznaju sa JavaScript-om (i pri tom već imaju iza sebe neophodno predznanje i dovoljno iskustva), a biće i svojevrsna rekapitulacija najvažnijih tehnika koje su deo ES6 sintakse, o kojima smo već pisali u drugim člancima.
Najvažnijim unapređenjima koje je donela verzija jezika ES6 smatramo arrow funkcije, šablonske niske i promise ("obećanja"), ali, počećemo od nečeg osnovnijeg ....
Novi način označavanja promenljivih - let i const
Iako se može reći da 'stari' način definisanja promenljivih (preko rezervisane reči var
), uglavnom nije zadavao prevelike probleme programerima, svakako je imao određenih nedostataka.
Pre svega:
- nije bilo moguće definisati konstante
- nije bilo moguće definisati promenljive koje važe samo unutar određenog bloka programskog koda (tj. samo 'unutar vitičastih zagrada')
Deklaracija podataka preko rezervisanih reči const
i let
otklanja navedene nedostatke i proširuje mogućnosti.
const
Kada se pri deklaraciji podatka koristi rezervisana reč const
, deklarisani podatak ima sledeće osobine:
- mora se inicijalizovati odmah pri deklaraciji
- nije moguće naknadno menjati vrednost *
- ako se deklaracija pojavi unutar bloka (tj. između vitičastih zagrada), dati blok predstavlja opseg definisanosti podatka
Poslednja stavka iz gornjeg nabrajanja, bitna je i zanimljiva: promenljive koje su definisane preko rezervisane reči var
prepoznaju samo globalni opseg i opseg funkcije, dok rezervisane reči const
i let
omogućavaju deklaraciju promenljivih čiji je opseg (npr) telo while
ili for
petlje, jedan od blokova u if
grananju i sl.
Što se tiče dodele vrednosti, važe sledeća pravila: vrednost koja je dodeljena (npr) celobrojnoj promenljivoj, doslovno se ne može menjati naknadno, dok je situacija sa nizovima i objektima nešto komplikovanija (ali i dalje sasvim razumljiva).
U okviru složenih objekata koji su deklarisani preko rezervisane reči const
, "konstantna" je referenca na objekat, dok se (unutrašnje) stanje objekta može menjati, ali, bez naknadnog pozivanja naredbe dodele za (ceo) objekat.
Na sledećoj slici vidimo primere koji pokazuju šta se može (a šta ne može) raditi sa podacima koji su deklarisani preko rezervisane reči const
:
// Inicijalizacija se mora obaviti
// u istoj naredbi sa deklaracijom:
const PI = 3.14159265359; // OK
const g; // GREŠKA
g = 9.81; // GREŠKA
// U inicijalizovanim nizovima, elementi
// se mogu dodavati i uklanjati, ali,
// referenca na niz se ne sme menjati:
const niz = [ 1, 2, 3, 4 ]; // OK
niz.push(5); // OK
niz = [ 2, 3, 7, 13 ] // GREŠKA
// Vrednosti polja objekata mogu se menjati, ali,
// ne sme se menjati referenca (tj. ne sme se
// naknadno koristiti naredba dodele):
const osoba = {
ime: "Petar",
prezime: "Milinković"
} // OK
osoba.ime = "Marija" // OK
osoba = {
id: 1,
uid: "korisnikX",
status: admin
} // GREŠKA
// Deklaracija preko rezervisane reči const poštuje
// opseg bloka koji je uokviren vitičastim zagradama:
if (a > 10) {
const p = "Proba";
console.log(p); // OK
}
console.log(p); // GREŠKA
let
Promenljive deklarisane preko rezervisane reči let
imaju sledeće osobine:
- jednom deklarisana promenljiva, ne sme se ponovo deklarisati
- vrednost promenljive može se menjati naknadno
- inicijalizacija se ne mora obaviti pri deklaraciji
- ako se deklaracija pojavi unutar bloka (između vitičastih zagrada), dati blok koda predstavlja opseg definisanosti promenljive
Ponovo ćemo pogledati primer koji prikazuje navedene odlike:
// Inicijalizacija se može, ali i ne mora,
// obaviti u istoj naredbi sa deklaracijom:
let PI; // OK
PI = 3.14159265359; // OK
// U inicijalizovanim nizovima, elementi se mogu
// dodavati i uklanjati, a može se koristiti i
// naredba dodele nakon inicijalizacije:
let niz = [ 1, 2, 3, 4 ]; // OK
niz.push(5); // OK
niz = [ 2, 3, 7, 13 ]; // OK
// Vrednosti polja objekata mogu se menjati, a može se
// naknadno koristiti i naredba dodele (za ceo objekat):
let osoba = {
ime: "Petar",
prezime: "Milinković"
} // OK
osoba.ime = "Marija" // OK
osoba = {
id: 1,
uid: "korisnikX",
status: admin
} // OK
// Deklaracija preko rezervisane reči let
// (takođe) poštuje opseg bloka koji je
// uokviren vitičastim zagradama:
if (a > 10) {
let p = "Proba";
console.log(p); // OK
}
console.log(p); // GREŠKA!
Arrow funkcije
U članku o callback funkcijama i lambda izrazima detaljno smo predstavili tzv. 'streličaste funkcije' (eng. arrow functions), i zato se ovoga puta nećemo previše zadržavati na detaljima, ali, podsetićemo se na najvažnije smernice.
Arrow funkcije predstavljaju specifičnu implementaciju lambda izraza u JavaScript-u i omogućavaju da se određena imenovana funkcija, koja je za određene potrebe (možda) previše "zvanična" i opširna, zapiše na jednostavniji način.
Za primer možemo uzeti funkciju koja vraća veću od dve unete vrednosti:
function veciOdDva(a, b) {
return (a > b)? a: b;
}
Ako se izostavi rezervisa reč function
i naziv funkcije, a potom se između parametara i tela funkcije doda 'lambda operator' =>
....
(a, b) => {
return (a > b)? a: b;
}
.... rezultat je arrow funkcija.
Budući da funkcija ima samo jednu naredbu (koja pri tom sadrži rezervisanu reč return
), funkcija se može dodatno pojednostaviti:
- izostavljanjem vitičastih zagrada
- izostavljanjem rezervisane reč
return
- time što će parametri i telo funkcije biti zapisani u istom redu
(a, b) => (a > b)? a: b
Sintaksa deluje 'kao stvoreno' za povratne pozive i pri tom - što i jeste glavna ideja koja stoji iza lambda zapisa - sve je veoma pregledno i elegantno.
Na primer, umesto imenovane funkcije:
function kvadrat(x) {
return x * x;
}
let niz1 = [ 1, 2, 3, 4, 5 ];
let niz2 = niz1.map(kvadrat);
.... ili standardne neimenovane funkcije:
let niz1 = [ 1, 2, 3, 4, 5 ];
let niz2 = niz1.map(function() {
return x * x;
});
.... funkcija map
može koristiti jednostavnu i elegantno zapisanu arrow funkciju:
let niz1 = [ 1, 2, 3, 4, 5 ];
let niz2 = niz1.map(x => x * x);
Naravno, povratni pozivi se mogu koristiti i u drugim funkcijama - ukoliko su zadovoljeni sledeći uslovi:
- potrebno je da definicija streličaste funkcije bude zapisana na mestu na kome se (streličasta) funkcija poziva
- potrebno je da pozivajuća funkcija omogućava korišćenje povratnih poziva
Streličaste funkcije se (inače) mogu povezivati i sa imenovanim konstantama, ali, o tome možete pročitati više u članku o callback funkcijama i lambda notaciji.
U svakom slučaju, lambda notacija (očigledno) predstavljaju veoma zanimljivo rešenje koje je široko rasprostranjeno u standardnim web aplikacijama koje koriste JavaScript, a pogotovo u Node.js projektima.
Šablonske niske ("pattern literals")
Šablonske niske su posebno formatirani tekstualni obrasci koji se zapisuju između znakova ``
("backtick"), i služe za umetanje vrednosti promenljivih i izraza u niske.
Na primer, pokretanjem sledećeg koda ....
let a = 5, b = 10;
let s = `Zbir brojeva ${a} i ${b} je ${a + b}`
console.log(s);
.... u konzoli će biti ispisano:
Zbir brojeva 5 i 10 je 15
Ili, malo 'konkretniji' primer (nalik primerima koji se svakodnevno koriste u radnim okruženjima kao što su Express, React, Vue i sl):
let paketPodataka = {
naslov: "Šablonske niske",
tekst: "Šablonske niske omogućavaju umetanje vrednosti promenljivih i izraza u tekst"
}
function formatiranjeHTMLElementa(paket) {
return
`
<h2>${paket.naslov}</h2>
<p>
${paket.tekst}
</p>`;
}
console.log(formatiranjeHTMLElementa(paketPodataka));
Promisi ("obećanja")
Za razliku od lambda notacije i arrow funkcija, što su koncepti koje relativno lako možemo predstaviti u nekoliko reči (čitaocima koji poznaju osnove jezika), 'promisi' nisu jednostavni za 'brzinsko upoznavanje'.
Ali, pokušaćemo ....
Ukratko (i pre svega), u pitanju je način da se nepregledni lanac funkcija povratnog poziva (u kome izvršavanje određene funkcije zavisi od rezultata izvršavanja prethodne funkcije) ....
setTimeout(() => {
if (!f1()) return new Error("Funkcija f1 nije se izvršila pravilno"):
setTimeout(() => {
if (!f2()) return new Error("Funkcija f2 nije se izvršila pravilno"):
setTimeout(() => {
if (!f3()) return new Error("Funkcija f3 nije se izvršila pravilno"):
setTimeout(() => {
if (!f4()) return new Error("Funkcija f4 nije se izvršila pravilno"):
setTimeout(() => {
if (f5()) {
console.log("Čestitamo, uspeli ste!");
}
else {
console.error("Gre'ota! Niste imali sreće na samom kraju!")
}
}, INTERVAL_5);
}, INTERVAL_4);
}, INTERVAL_3);
}, INTERVAL_2);
}, INTERVAL_1);
.... zapiše znatno preglednije:
f1()
.then(f2)
.then(f3)
.then(f4)
.then(f5)
.then(rez => krajnjaObrada(rez))
.catch(greska => obradaGreske(greska));
Kada su u pitanju promisi, sa standardom ES7 (iz 2017), došla je i async/await
sintaksa (koju ćemo iz praktičnih razloga pomenuti, iako očigledno nije deo standarda ES6):
try {
let rez1 = await f1();
let rez2 = await f2(rez1);
let rez3 = await f3(rez2);
let rez4 = await f4(rez3);
let rez5 = await f5(rez4);
krajnjaObrada(rez5);
}
catch (greska) {
obradaGreske(greska);
}
Moduli
U većim projektima, kada se javi potreba za podelom programskog koda na manje celine, 'srodni' delovi programskog koda mogu se rasporediti u više datoteka (tj. "modula").
Preko direktive export
(u okviru svakog modula), označavaju se funkcije koje će biti dostupne drugim modulima.
Ako za primer uzmemo da je skripta pomocne_funkcije.js
zapisana na sledeći način ....
function Maksimum(a, b) {
return (a >= b)? a : b;
}
export function Obim(a, b) {
return 2 * a + 2 * b;
}
export function Povrsina(a, b) {
return a * b;
}
.... funkcija Maksimum
može se koristiti samo u okviru modula u kome je definisana, ali, funkcije Obim
i Povrsina
mogu se koristiti (i) u drugim modulima ....
// Sadržaj datoteke app.js:
import { Obim, Povrsina } from './funkcije';
console.log(Obim(5, 10)); // 30
console.log(Povrsina(5, 10)); // 50
Operator ... (rest/spread)
Operator ...
koji se prepoznaje kao operator rest/spread (tačnije, parametar rest i operator spread), * može se koristiti za sledeće dve operacije:
- spajanje kolekcije pojedinačnih elemenata u jedinstvenu listu
- rastavljanje jedinstvene liste na pojedinačne elemente
Smisao operatora spread možemo razumeti preko sledećeg primera:
// Ukoliko definišemo tri niza ....
let niz1 = [ 1, 2, 3 ]
let niz2 = [ 4, 5, 6 ]
let niz3 = [ 7, 8, 9 ]
// .... i potom pokušamo da ih spojimo ....
let nizSpojeni = [ niz1, niz2, niz3 ]
// .... rezultat je niz nizova,
// slično kao da smo upotrebili
// sledeću naredbu dodele:
nizSpojeni = [
[ 1, 2, 3 ] ,
[ 4, 5, 6 ] ,
[ 7, 8, 9 ]
]
// .... međutim, ukoliko upotrebimo
// operator spread ....
nizSpojeni = [ ...niz1, ...niz2, ...niz3 ]
// .... rezultat je "običan" niz:
// slično kao da smo pozvali sledeću
// (drugačiju) naredbu dodele:
nizSpojeni = [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
Smisao parametra rest (koji je namenjen korišćenju u svojstvu parametra funkcije), biće objašnjen nešto kasnije, u odeljku o funkcijama sa proizvoljnim brojem parametara.
Destrukturiranje objekata i nizova
U JavaScript-u, termin "destrukturiranje" označava posebnu naredbu dodele koja omogućava:
- preuzimanje određenog podskupa podataka koji su zapisani u okviru objekta (ili)
- preuzimanje određenog podskupa elemenata niza
Destrukturiranje objekta prikazaćemo preko sledećeg primera: pojedinačna polja objekta osoba
....
let osoba = {
ime: "Petar",
prezime: "Petrović"
}
... mogu se 'izvući' iz objekta (tj. praktično - kopirati), i zatim se mogu povezati sa spoljnim promenljivama (pod uslovom da spoljne promenljive koriste nazive koji odgovaraju nazivima polja):
let { ime, prezime } = osoba;
console.log(ime); // Petar
console.log(prezime); // Petrović
Destrukturiranje niza prikazaćemo na sledećem primeru ....
let niz = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ];
let [ prvi, drugi, treci, ...ostali ] = niz;
console.log(prvi); // 1
console.log(drugi); // 2
console.log(treci); // 3
console.log(ostali); // [ 4, 5, 6, 7, 8, 9, 10 ]
.... pri čemu vidimo da je u pitanju nešto drugačiji pristup:
- nazivi promenljivih mogu biti proizvoljni
- uvažava se redosled elemenata (prva promenljiva povezuje se sa prvim elementom niza, druga sa drugim i sl)
- ukoliko je broj spoljnih promenljivih manji od broja elemenata niza - i pri tom se uz poslednju promenljivu navede rest parametar, poslednja promenljiva praktično postaje podniz koji (redom) čine svi elementi koji nisu izdvojeni u prethodne promenljive
Klase
Sa klasama u JavaScript-u već smo se sretali (u više navrata), ali, pomenućemo ih ukratko i na ovom mestu, budući da su klase uvedene u JavaScript upravo sa revizijom ES6.
Klasa predstavlja šablon za kreiranje (budućeg) objekta:
class Osoba
{
constructor(ime, prezime) {
this.ime = ime;
this.prezime = prezime;
}
ispis() {
return `ime: ${this.ime}
prezime: ${this.prezime}`
}
}
.... koji se može instancirati:
let osoba = new Osoba("Petar", "Milinković");
.... i koristiti dalje u kodu (kao objekat sa poljima i metodama) ....
let s = osoba.ispis()
console.log(s)
Podrazumevane vrednosti parametara funkcija
Počevši od revizije ES6, JavaScript omogućava korišćenje podrazumevanih vrednosti za parametre funkcija (prepoznaju se po naredbi dodele):
function zbir(a, b = 15) {
return a + b;
}
console.log(zbir(10, 12)); // 22
console.log(zbir(10)); // 25
Funkcije sa proizvoljnim brojem parametara
Funkcija koja ja deklarisana na sledeći način (uz korišćenje rest parametra):
function suma(...parametri) {
let s = 0;
for (let p of parametri) {
s += p;
}
return p;
}
.... može se pozvati na sledeći način:
console.log(sum(1, 2, 19, 74, 211)); // 307
.... pri čemu se predati argumenti tretiraju kao niz - koji može sadržati proizvoljan broj elemenata.
Nove matematičke funkcije (cbrt, sign, trunc, log2, log10)
Sa standardnom ES6, pojavile su se i nove matematičke funkcije:
/* -------------------------------------------- */
// Kubni koren:
Math.cbrt(8); // 2
/* -------------------------------------------- */
// Predznak broja:
Math.sign(-10); // -1
/* -------------------------------------------- */
// Odbacivanje decimalnih cifara:
Math.trunc(4.51); // 4
/* -------------------------------------------- */
// Logaritam broja a za osnovu 2:
Math.log2(32); // 5
/* -------------------------------------------- */
// Logaritam broja a za osnovu 10:
Math.log10(100); // 2
/* -------------------------------------------- */
Operator stepenovanja
Za kraj, jedna sitnica, koja, iako ne predstavlja 'preveliko unapređenje' - svakako dobro dođe.
Jezik JavaScript je i ranije raspolagao funkcijom za računanje stepena broja ....
// Primmer: funkija vraća četvrti
// stepen broja dva
let a = pow(2, 4); // rezultat: 16
.... međutim, nije postojao način da se operacija stepenovanja zapiše na neposredan način (osim ako ne računamo 'iznuđene' i ne-baš-pregledne načine zapisa):
let a = 2 * 2 * 2 * 2;
Počevši od revizije ES6, naredba koja računa npr. četvrti stepen broja 2 (kao u gornjim primerima), može se zapisati na primetno jednostavniji način ....
let a = 2 ** 4;
.... preko specijalizovanog operatora.
Kratak rezime ....
Članak o ES6 sintaksi predstavlja (kao što smo na početku najavili), kratku rekapitulaciju novih tehnika i "usputnu stanicu" (pre detaljnijeg udubljivanja), za pojedince koji imaju (dovoljno) prethodnog iskustva sa osnovama JavaScript-a - i odlučili su da savladaju Node.Js, Express, React, Vue ili neku od drugih popularnih i široko rasprostranjenih tehnologija.
Međutim, ne moramo se obavezno usmeriti na Node.js ("i njegovu decu"), već možemo sve tehnike o kojima smo pisali posmatrati i nezavisno, kao pojave koje su veoma zanimljive same po sebi, i pri tom predstavljaju deo jezika koji je često osporavan, ali je sa vremenom postajao sve bolji i bolji.
U svakom slučaju, posle upoznavanja sa ES6 sintaksom, otvaraju se brojni putevi.
U članku smo pomenuli samo neke (one najpopularnije), a vama ostavljamo da savladate tehnike koje smo predstavili, čime ćete sebi znatno olakšati rad u modernim razvojnim okruženjima koja kao programski jezik koriste JavaScript ....