Uvod
Nizovi u JavaScript-u deklarišu se i inicijalizuju na vrlo komotan način, ne podležu strogoj tipizaciji (niz može sadržati elemente različitog tipa), interna implementacija nizova je prilično kompleksna, a ako bismo baš hteli - našli bismo bar još koju 'zamerku'. :)
Međutim, mora se priznati da, u radu sa nizovima u JS-u, ima šarma i lepote (ima i svega navedenog iz prethodne rečenice; nismo zaboravili), i mora se priznati da u većini situacija u praksi - naravno, pod uslovom da "pazimo šta radimo" - stvari funkcionišu prilično glatko.
Pristup koji predlažemo čitaocima (i koji primenjujemo u praksi), podrazumeva da - kada već programski jezik dozvoljava "sve i svašta" - vođenje računa o bitnim stvarima preuzmemo na sebe i sa nizovima postupamo onako kako bismo postupali da koristimo jezik sa strogom tipizacijom.
U takvim okolnostima, JavaScript nam neće priređivati neprijatna iznenađenja.
Nizovi u JavaScript-u uopšteno
Za početak, valja razmotriti šta se (uopšte) u JavaScript-u podrazumeva pod pojmom "niz", jer krajnji rezultat zavisi od endžina koji se koristi, * pa ne možemo biti sigurni šta se dešava "ispod haube", gde "niz" može biti zapisan: bilo kao statički niz, bilo kao hash-mapa, sve u zavisnosti od okolnosti.
Ovoga puta, bićemo praktični i nećemo ulaziti u detaljniju diskusiju o implementaciji nizova (ostavićemo to za neku drugu priliku), ali, za sada ćemo definisati nizove u JavaScript-u kao posebne promenljive koje omogućavaju smeštanje kolekcija podataka preko jednog identifikatora (naziva promenljive), pri čemu podaci ne moraju biti istog tipa, i pri čemu je moguće proizvoljno dodavati ili uklanjati elemente.
Deklaracija
Osnovni vid deklaracije niza u JS-u (praktično) podrazumeva i inicijalizaciju praznog niza u istoj liniji koda (jer, ukoliko izostavimo inicijalizaciju, promenljiva neće biti referenca na (prazan) niz, već objekat sa sistemskom vrednošću null
):
let niz = [ ];
Niz je prazan, ali, svakako se u daljem toku izvršavanja programa mogu dodavati novi elementi.
Inicijalizacija
Pored inicijalizacije praznog niza (kakvu smo već videli), često * se javlja i inicijalizacija/deklaracija koja podrazumeva zadavanje (nekoliko) elemenata:
let niz = [ 1, 2, 2, 3, 4, 5, 5 ];
let imena = [ "Milan", "Petar", "Jovan" ];
Takođe, niz može biti inicijalizovan i preko funkcija koje vraćaju nizove.
let niz = funkcijaKojaVracaNiz(ulaz);
// Primer:
let s = "papir;kamen;makaze";
let lista = s.split(";"); // [ "papir" , "kamen" , "makaze" ]
Što se tiče inicijalizacija koje smo videli na slici #3, na scenu stupa "preuzimanje odgovornosti sa naše strane".
Ako za inicijalizaciju koristimo funkciju koju smo definisali sami, nećemo prihvatiti "zdravo za gotovo" da će funkcija svaki put vratiti uredno inicijalizovan niz, već ćemo se sami postarati da - ukoliko dođe do greške - funkcija ponudi adekvatnu informaciji o pojavi greške, na jedan od sledeća dva načina:
- preko povratne vrednosti po kojoj možemo prepoznati da je došlo do greške (na primer, povratna vrednost može biti prazan niz - u situaciji u kojoj smo očekivali niz sa izvesnim brojem elemenata)
- preko odgovarajućeg izuzetka (u kom slučaju je potrebno da koristimo blok
try-catch
)
Ukoliko ne koristimo DIY funkcije, već koristimo gotova rešenja, konsultovaćemo dokumentaciju, i naravno - bićemo pažljivi.
Pristup elementima
Pristup elementima niza, obavlja se na način koji je uobičajen u programskom jeziku C (navođenje indeksa u velikoj zagradi): *
let imenaNiz = [ "Dejan" , "Milan" , "Jovan" ];
let imeOsobe = imenaNiz[2]; // "Jovan"
Indeksiranje elemenata takođe je rešeno po uzoru na C, što praktično znači da prvi element ima indeks 0 (a ne 1). **
Očitavanje dužine niza (length)
Dužina niza (tj. broj elemenata), može se očitati preko svojstva length
:
let d = niz.length;
Sada možemo (recimo), napraviti i for
petlju koja pristupa svim elementima:
for (let i = 0; i < d; ++i) {
niz[i] = niz[i] + 1;
}
Dodavanje elementa na kraj niza (push)
Za dodavanje elementa na kraj niza, koristi se funkcija push
:
// [ 1, 2, 2, 3, 4, 5, 5 ]
niz.push(10); // [ 1, 2, 2, 3, 4, 5, 5, 10 ]
Za dodavanje elementa (ili elemenata) na proizvoljnu poziciju, koristi se funkcija splice
(više o funkciji splice
u nastavku).
Uklanjanje elemenata sa početka ili kraja niza (pop i shift)
Kada je u pitanju uklanjanje elemenata, razlikujemo tri slučaja:
- uklanjanje elementa sa kraja niza
- uklanjanje elementa sa početka niza
- uklanjanje elementa sa proizvoljne pozicije
Element sa kraja niza, uklanja se preko funkcije pop
:
// niz: [ 1, 2, 2, 3, 4, 5, 5, 10 ]
let p = niz.pop();
// p: 10
// niz: [ 1, 2, 2, 3, 4, 5, 5 ]
Element sa početka niza, uklanja se preko funkcije shift
:
// niz: [ 1, 2, 2, 3, 4, 5, 5 ]
let p = niz.shift();
// p: 1
// niz: [ 2, 2, 3, 4, 5, 5 ]
Za uklanjanje elementa sa proizvoljne pozicije, koristi se (već pomenuta) funkcija splice
, s tim da je u pitanju univerzalna funkcija preko koje je moguće obaviti: i dodavanje, i uklanjanje.
Funkcija splice - dodavanje i/ili uklanjanje elemenata na proizvoljnoj poziciji
Poslednje što smo naveli u prethodnom odeljku, može delovati pomalo čudno, ali, funkcija splice
je veoma univerzalna i može se koristiti za obe operacije:
niz.splice(indeks, n_uklanjanje, elementi_za_dodavanje ....);
// indeks - indeks na kome se obavlja dodavanje, ili
// od koga počinje uklanjanje
// n_uklanjanje - broj elemenata koje treba ukloniti
// elementi - lista elemenata koji će biti dodati
Kao što vidimo, zaista veoma univerzalno, ali - kako to (sve) funkcioniše u praksi?!
Dodavanje elemenata
Za dodavanje elemenata, potrebno je postaviti indeks na poziciju koja odgovara mestu na kome želimo da se pojavi prvi element iz liste novih elemenata i takođe, potrebno je naglasiti da se nijedan element ne uklanja (drugi argument je 0).
Od trećeg argumenta počinje lista elemenata koji se umeću.
Ako je i
indeks za umetanje, a n
broj elemenata za umetanje, cela operacija podrazumeva umetanje liste od n
elemenata između pozicija i
i i + 1
.
let niz = [ 1 , 2 , 3 , 4 , 5 ]
niz.splice(2, 0, 1001, 1002)
// [ 1 , 2 , 1001 , 1002 , 3 , 4 , 5 ]
U konkretnom primeru, između druge i treće pozicije, umeću se dva nova elementa (1001, 1002).
Uklanjanje elemenata
Za uklanjanje elemenata, potrebno je navesti dva parametra: indeks prvog elementa koji se uklanja i ukupan broj elemenata koje je potrebno ukloniti.
let niz = [ 1 , 2 , 3 , 4 , 5 ]
niz.splice(2, 1)
// [ 1 , 2 , 4 , 5 ]
U konkretnom primeru, na trećoj poziciji (indeks 2), uklonili smo jedan element.
Kombinovani pristup
Po potrebi, prethodne dve operacije moguće je kombinovati (pri čemu se obavezno predaje tri ili više argumenata).
Prvo se uklanja određen broj elemenata, počevši od indeksa koji je naveden kao prvi argument (uklanja se onoliko elemenata koliko je navedeno preko drugog argumenta), a zatim se umeću novi elementi, * na mesto koje (praktično) odgovara poziciji na kojoj je došlo do uklanjanja.
let niz = [ 1 , 2 , 3 , 4 , 5 ]
niz.splice(2, 2, 2001, 2002)
// [ 1 , 2 , 2001, 2002, 5 ]
U konkretnom primeru, prvo se uklanjaju elementi sa indeksima 2 i 3 (dva elementa, počevši od indeksa 2), a zatim se između novih indeksa 1 i 2 (ili uslovno rečeno, između prvobitnih indeksa 1 i 4), umeću novi elementi (2001 i 2002).
Spajanje nizova (concat)
U JavaScript-u, dva niza mogu se spojiti preko funkcije concat
, na sledeći način:
let a = [ 1, 2, 3 ];
let b = [ 4, 5, 6 ];
let c = a.concat(b); // [ 1, 2, 3, 4, 5, 6 ]
Spajanje niza u nisku znakova (join)
Ukoliko je potrebno niz elemenata spojiti u nisku, može se koristiti funkcija join
:
let a = [ "N", "i", "z", "o", "v", "i" ];
let s1 = a.join(); // "N,i,z,o,v,i"
let s2 = a.join("*"); // "N*i*z*o*v*i"
let s2 = a.join(""); // "Nizovi"
Ukoliko se funkciji join
ne preda argument, rezultujuća niska nastaje direktnim spajanjem elemenata niza, uz dodavanje zareza između svaka dva susedna elementa.
Ukoliko se, kao argument, preda niska (koja predstavlja crtu, prelazak u novi red i sl), predata niska se (u okviru rezultujuće niske), pojavljuje između svaka dva elementa prvobitnog niza (videti gornji primer).
U obradi tekstualnih datoteka, funkcija join
(baš kao i funkcija split
, koju ćemo prikazati u narednom odeljku), koristi se često.
Pretvaranje niske znakova u niz (split)
Deljenje niske obavlja se preko funkcije split
, rezultat je lista elemenata, a kao kriterijum za podelu, može se koristiti određena niska ili regularni izraz:
let s = "Pariz-London-Lisabon";
let a = s.split("-"); // [ "Pariz", "London", "Lisabon" ]
Kao što je pomenuto, funkcije join
i split
veoma dobro dođu u obradi teksta, a da bismo što bolje ilustrovali upotrebnu vrednost navedenih funkcija, napisaćemo jednostavnu skriptu čiji je zadatak da pravilno formatira listu imena u kojoj se separatori (znakovi za razdvajanje), ne koriste na dosledan način:
// Dobre informacije, ali, nedosledna
// upotreba znakova za razdvajanje:
let s = "Milan Ivan, Jovan # Dejan";
// Regex je praktično kolekcija svih "razdvajača":
let regex = /([ ,#])/
let lista = s.split(regex); // [ "Milan", " " , "Ivan", ",",
// " ", "Jovan", " ", "#",
// " " , "Dejan" ]
// Napomena: u praksi, lista bi sadržala i nekoliko
// praznih stringova ("")
let nova_lista = [];
for (let i = 0; i < lista.length; ++i) {
// Ako je element liste prazna niska ili neki
// od separatora, nećemo takav element ubacivati
// u novu listu:
if (lista[i] == "" || lista[i].match(regex)) {
continue;
}
// Sve ostale elemente (praktično - samo imena),
// ubacićemo u novu listu:
nova_lista.push(lista[i]);
}
// Na kraju, spojićemo listu u ("običnu") nisku,
// u kojoj će svako ime biti zapisano u novom redu
let s2 = nova_lista.join("\n");
Nisku smo rastavili preko funkcije split
, prošli smo kroz sve elemente liste (koja je nastala rastavljanjem niske), izdvojili smo imena i na kraju smo od liste formirali ("običnu") nisku, preko funkcije join
.
Funkcije višeg reda za rad sa nizovima
Funkcije koje smo nabrojali u članku, nisu naravno sve funkcije koje se u JS-u koriste za rad sa nizovima (ni iz daleka), već samo one koje smatramo najkorisnijim za sam početak.
Međutim, iako smo članak namenili čitaocima sa 'ne-baš-mnogo' iskustva, osvrnućemo se i na nekoliko (ponešto) naprednijih funkcija za rad sa nizovima, koje se obično koriste u sprezi sa tzv. "lambda" notacijom.
Ne dajte da vas navedeni termini 'zaplaše', jer, u pitanju je prilično jednostavan način zapisa koji većina programera (čak i mlađih i/ili neiskusnijih), može lako prihvatiti intuitivno, a kada bude došlo vreme, možete se detaljno upoznati sa sintaksom funkcija povratnih poziva i lambda izraza.
every
Sledeći kod ....
a.every(x => x > 1)
.... vraća vrednost true
pod uslovom da su svi elementi niza veći od 1 (u suprotnom, povratna vrednost je false
).
some
Sledeći kod ....
a.some(x => x > 10)
.... vraća vrednost true
pod uslovom da je bar jedan element niza veći od 10 (u suprotnom, povratna vrednost je false
).
map
Ukoliko je potrebno kopirati niz, pri čemu se novim elementima dodeljuju određene vrednosti preko funkcije, može se koristiti sledeća sintaksa:
let a = [ 1, 2, 3, 4, 5 ];
let b = a.map(x => x * x);
Pokretanjem poslednje skripte, nastaće novi niz sa vrednostima [ 1, 4, 9, 16, 25 ]
(koje predstavljaju kvadrate vrednosti iz ulaznog niza).
filter
Ukoliko je - pri kopiranju niza - potrebno ukloniti određene vrednosti, može se koristiti funkcija filter
.
Cela operacije podrazumeva kreiranje novog niza, u koji će biti kopirane samo vrednosti koje zadovoljavaju uslov:
let a = [ 1, 2, 3, 4, 5 ];
let b = a.filter(x => x > 3);
U primeru sa gornje slike, novi niz sadržaće samo elemente 4 i 5.
forEach
Za pojednostavljeni zapis petlje koja prolazi kroz ceo niz, možemo (kao što smo već nagovestili), koristiti sledeću sintaksu:
let a = [ 1, 2, 3, 4, 5 ];
a.forEach(element => {
console.log(element);
});
Primećujemo da se pri korišćenju funkcije forEach
, elementima ne pristupa preko indeksa, već preko reference element
(s tim da je u pitanju proizvoljno izabran identifikator, a ne rezervisana reč i sl).
Za sam kraj, ostavili smo funkciju za sortiranje nizova, koja je u idejnom smislu jednostavna, ali - u velikoj meri zavisi od povratnih poziva i lambda izraza.
Sortiranje nizova (sort)
Kada je u pitanju uređivanje nizova brojčanih vrednosti u neopadajući ili nerastući poredak, može se primetiti da su programeri vrlo često skloni tome da samostalno implementiraju funkcije koje rešavaju navedene probleme.
Javascript pruža mogućnost korišćenja ugrađene funkcije za sortiranje, međutim, bitno je odmah razumeti da je u pitanju funkcija koja, prema podrazumevanim podešavanjima (to jest, ako se ne preda nikakav argument), uređuje niz po abecednom redosledu. *
let a = [ "Maja", "Ana", "Pera", "Mika" ];
a.sort(); // [ "Ana", "Maja", "Mika", "Pera"]
let b = [ 2, 10, 3, 5, 1, 6, 1, 4 ];
b.sort(); // [ 1, 1, 10, 2, 3, 4, 5, 6]
Da bi niz bio sortiran u nerastući poredak prema brojčanim vrednostima, potrebno je da (u svojstvu argumenta), funkciji sort
predamo - funkciju.
Korišćenje funkcija povratnog poziva je veoma zanimljiva ideja - koju smo donekle 'provukli' kroz dosadašnje izlaganje i kojom ćemo se (kao što smo najavili), opširnije baviti drugom prilikom, ali, pokušaćemo da ukratko objasnimo ideju koja stoji iza poziva funkcije sort
koje ćemo u nastavku prikazati.
U funkcijama za sortiranje nizova, često se javlja 'motiv' poređenja dva elementa niza, čije vrednosti potom treba (ili ne treba) razmeniti, a sam uslov za razmenu dva elementa, zapisan je u telu funkcije tako da se ne može menjati tokom izvršavanja programa ("tako je inače").
Funkcija sort
dozvoljava da joj se kao argument preda funkcija - preko koje će biti određeno da li dva elementa koji se porede, treba ili ne treba razmeniti.
Ako napravimo sledeći poziv (uz korišćenje pomoćne funkcije provera
, koju ćemo samostalno implementirati):
function provera(a, b) {
return a - b;
}
let a = [ 2, 10, 3, 5, 1, 6, 1, 4 ];
a.sort(provera) // 1, 1, 2, 3, 4, 5, 6, 10
.... niz će biti sortiran u neopadajući poredak (naravno, ovoga puta - prema brojčanim vrednostima).
Za sortiranje niza brojčanih vrednosti u nerastući poredak, biće dovoljno da funkciju provera
implementiramo na drugačiji način:
function provera(a, b) {
return b - a;
}
let a = [ 2, 10, 3, 5, 1, 6, 1, 4 ];
a.sort(provera); // 10, 6, 5, 4, 3, 2, 1, 1
Iako nije u pitanju trivijalan programski kod, ideja koji stoji iza poziva koje smo prikazali je (ipak), relativno jednostavna.
Ukoliko pomoćna funkcija za proveru vrati negativan broj - doći će do razmene (u suprotnom, neće doći do razmene).
U prvom slučaju, do razmene dolazi ako je argument b
veći, dok u drugom slučaju, do razmene dolaze ukoliko je veći argument a
.
Preko lambda notacije, pozivi se mogu dodatno uprostiti:
a.sort((a, b) => a - b) // uređivanje niza u neopadajući poredak
a.sort((a, b) => b - a) // uređivanje niza u nerastući poredak
Sve što smo prikazali (u vezi sa korišćenjem povratnih poziva i lambda izraza u ugrađenoj funkciji sort
), "funkcioniše" - samo zato što je sama funkcija projektovana tako da, u telu funkcije, koristi funkcije koje se predaju kao argument.
Pre nego što se 'odjavimo', napomenimo da - iako su kodovi koje smo prikazali prilično intuitivni, gradivo koje je prikazano u članku o funkcijama za rad sa nizovima u JS-u, ni iz daleka nije dovoljno za postizanje pravog razumevanja u vezi sa funkcijama povratnog poziva i lambda notacijom, i stoga preporučujemo da - kada dođe vreme - detaljnije proučite članak koji smo posvetili navedenim temama.
Kratak rezime ....
Kao što smo videli, rad sa nizovima u JS-u nije nikakav "bauk" (naravno, uz opasku da svakako moramo voditi više računa nego u strogo tipiziranim jezicima, gde ipak postoje određeni mehanizmi koji će nas sprečiti da u niz celobrojnih vrednosti dodajemo niske i slično).
Sledeći članak o JS-u posvetićemo implementaciji čvorova i struktura podataka ....