Operacije sa nizovima u programskom jeziku JavaScript
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 relativno kompleksna, a ako bismo baš hteli, našli bismo bar još koju 'zamerku'. :)
Međutim, mora se priznati da ima 'šarma i lepote' u radu sa nizovima u JS-u (ima i svega navedenog iz prethodnog pasusa; nismo zaboravili), i mora se priznati da, u većini uobičajenih situacija (naravno - pod uslovom da "pazimo šta radimo"), stvari zapravo funkcionišu prilično glatko.
Pristup koji predlažemo čitaocima (i koji primenjujemo u praksi), podrazumeva sledeće: kad već programski jezik dozvoljava "sve i svašta", uputno je da preuzmemo na sebe vođenje računa o bitnim stvarima, i postupamo sa nizovima onako kako bismo postupali da koristimo jezik sa strogom tipizacijom, a onda (ukoliko postupamo (dovoljno) pažljivo), JavaScript neće priređivati neprijatna iznenađenja ....
Osnovne operacije sa nizovima
Za početak (pre nego što se posvetimo osnovnim operacijama za rad sa nizovima), ukratko ćemo razmotriti šta se (uopšte) u JavaScript-u podrazumeva pod pojmom "niz", jer krajnji rezultat zavisi od endžina koji se koristi * i stoga 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 (i tako da programeri i krajnji korisnici nemaju uticaja na izbor formata).
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 niz u JavaScript-u definisati kao poseban tip promenljive koja omogućava 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 niza
Osnovni vid deklaracije niza u JS-u, praktično podrazumeva i inicijalizaciju praznog niza u istoj liniji koda (jer, ukoliko izostane inicijalizacija, 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 niza
Pored inicijalizacije praznog niza (što smo videli u prethodnom odeljku), č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" ];
Nizovi se takođe mogu inicijalizovati i preko funkcija (čija je povratna vrednost referenca na početak niza).
// Primer #1:
let niz = funkcijaKojaVracaNiz(ulaz);
// Primer #2:
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 se za inicijalizaciju koristi funkcija koju smo definisali sami, ne treba prihvatati "zdravo za gotovo" da će funkcija svaki put vratiti uredno inicijalizovan niz, to jest, treba se postarati da funkcija - ukoliko dođe do greške - ponudi adekvatnu informaciji o pojavi greške, na jedan od sledeća dva načina:
- preko povratne vrednosti po kojoj se može prepoznati da je došlo do greške (na primer, povratna vrednost može biti prazan niz - u situaciji u kojoj je očekivani rezultat bio niz sa izvesnim brojem elemenata)
- preko odgovarajućeg izuzetka (u kom slučaju je potrebno koristiti blok
try-catch)
Ukoliko umesto DIY funkcija koristimo gotova rešenja, potrebno je konsultovati dokumentaciju i (naravno) - potrebno je biti pažljiv.
Pristup elementima
Pristup elementima niza obavlja se na način koji je uobičajen u programskom jeziku C: u velikoj zagradi, navodi se indeks (tj. 'redni broj traženog elementa').
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 (tj. nema indeks 1). **
Očitavanje dužine niza (length)
Dužina niza (tj. broj elemenata), očitava se 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, posle opisa funkcija pop i shift).
Uklanjanje elemenata sa početka ili kraja niza (pop i shift)
Kada je u pitanju uklanjanje elemenata, razlikuju se 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, s tim što preostaje pitanje kako to (sve) funkcioniše u praksi?!
Dodavanje elemenata
Za dodavanje elemenata, potrebno je postaviti indeks na poziciju koja odgovara mestu na kome treba 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 i 1002).
Uklanjanje elemenata
Za uklanjanje elemenata, potrebno je navesti dva argumenta koji (redom) predstavljaju 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), uklonjen je 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đeni 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 (pogotovo u jednostavnijim situacijama), dva niza se tipično spajaju 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)
Za spajanje niza elemenata u nisku, tipično * se koristiti funkcija join:
- ukoliko se funkciji
joinne preda argument, rezultujuća niska nastaje direktnim spajanjem elemenata niza, uz dodavanje zareza između svaka dva susedna elementa - ukoliko se funkciji
joinkao argument preda niska (koja predstavlja crtu, prelazak u novi red i sl), predata niska pojavljuje se u okviru rezultujuće niske, između svaka dva elementa prvobitnog niza (videti donji primer)
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"
U obradi tekstualnih datoteka, funkcija join koristi se često (baš kao i funkcija split koju ćemo prikazati u narednom odeljku).
Pretvaranje niske znakova u niz (split)
Deljenje niske obavlja se preko funkcije split, pri čemu se kao kriterijum za podelu koristi određena niska ili regularni izraz.
Pod uslovom da ulazna niska sadrži nisku koja se predaje kao argument (u donjem primeru: "-"), rezultat je lista 'okolnih podniski':
let s = "Pariz-London-Lisabon";
let a = s.split("-"); // [ "Pariz", "London", "Lisabon" ]
Kao što smo pomenuli, 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 (tj. znakovi za razdvajanje), ne koriste na dosledan način:
// Dobre informacije, ali, nedosledna
// upotreba znakova za razdvajanje:
let s = "Milan Ivan, Jovan # Dejan";
// Donji regularni izraz praktično predstavlja
// kolekciju 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;
}
// Svi ostali elementi (praktično - samo imena),
// ubacuju se u novu listu:
nova_lista.push(lista[i]);
}
// Na kraju, lista se spaja u ("običnu") nisku,
// u kojoj će svako ime biti zapisano u novom redu
let s2 = nova_lista.join("\n");
Skripta kreira listu (preko funkcije split), prolazi kroz sve elemente liste, izdvaja imena i, na kraju, od liste formira ("običnu") nisku, preko funkcije join.
Funkcije višeg reda za obavljanje operacija sa nizovima
Funkcije koje smo do sada prikazali u članku nisu (ni iz daleka) "sve" funkcije koje se u JS-u koriste za obavljanje operacija sa nizovima, već samo one koje smatramo najkorisnijim za sam početak (kao i većinu 'svakodnevnih' zadataka), međutim, iako smo članak namenili pre svega čitaocima sa 'ne-baš-mnogo' iskustva, osvrnućemo se i na nekolicinu (ponešto) naprednijih funkcija za rad sa nizovima, koje se obično koriste u sprezi sa tzv. "lambda notacijom".
U svakom slučaju, ne dajte da vas termini kao što su "funkcije višeg reda" i "lambda izrazi" uplaše i odvrate, jer ipak su u pitanju relativno jednostavne tehnike sa kojima se većina programera (čak i mlađih i/ili neiskusnijih), može na početku upoznati na jednostavan i intuitivan način (bez opterećenja i sl).
every
Naveli smo već da je sintaksa funkcija višeg reda uglavnom relativno jednostavna, * što možemo ilustrovati na primeru funkcije every (koja pristupa svakom elementu niza i poziva 'funkciju za obradu'):
// Opšta šema:
niz.every(funkcija_za_obradu)
// Funkcija every pristupa svakom elementu
// niza i primenjuje "funkciju za obradu"
// zarad provere uslova
// "Izolovani" primer:
niz.every(e => e > 1)
// e - pojedinačni element niza;
// funkcija every pristupa (redom) -
// svim elementima, i proverava,
// preko pomoćne lambda funkcije,
// da li je svaki element veći od 1
// Praktičan primer upotrebe:
if (niz.every(e => e > 1)) {
console.log("Svi su veći od 1.")
}
Funkcija every vraća vrednost true pod uslovom da svi elementi niza zadovoljavaju određeni uslov, dok je u suprotnom povratna vrednost false (u gornjem primeru, ispituje se da li su svi elementi niza veći od 1).
Sa jedne strane, kod deluje intuitivno (nadamo se :)), međutim, budući da (ipak) nisu u pitanju trivijalne ideje, potrudićemo se da odmah pojasnimo kako se navedeni kod interpretira.
Od svega je najbitnije razumeti da lambda notacija ** (kako u konkretnom primeru tako i inače), ne funkcioniše "sama od sebe", već, zato što je određena funkcija (u ovom slučaju funkcija every), osmišljena tako da "ispod haube" pristupa svim elementima niza - pri čemu koristi posebno definisanu "unutrašnju funkciju", ** koja je navedena u zagradi, preko koje se obavlja određena operacija (što u slučaju funkcije every znači - ispitivanje uslova).
some
Sledeći kod (po mnogo čemu sličan primerima iz prethodnog odeljka) ....
niz.some(e => e > 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 niz_a = [ 1, 2, 3, 4, 5 ];
let niz_b = niz_a.map(e => e * e);
Pokretanjem skripte, nastaje novi niz: [ 1, 4, 9, 16, 25 ].
Nove vrednosti predstavljaju kvadrate vrednosti iz ulaznog niza, i pri tom se funkcija preko koje se definišu nove vrednosti (i ovoga puta) predaje kao argument, u zagradi.
filter
Ukoliko je pri kopiranju niza potrebno ukloniti određene vrednosti, koristi se funkcija filter.
Cela operacije podrazumeva kreiranje novog niza, u koji će biti kopirane samo vrednosti koje zadovoljavaju uslov:
let niz_a = [ 1, 2, 3, 4, 5 ];
let niz_b = niz_a.filter(e => e > 3);
U primeru sa gornje slike, novi niz sadržaće samo elemente 4 i 5 (to jest, kopiraju se samo vrednosti koje su veće od 3).
forEach
Za pojednostavljeni zapis petlje koja prolazi kroz ceo niz, JavaScript omogućava korišćenje konstrukcije forEach.
Opšta šema poziva funkcije forEach ima sledeći oblik ....
niz.forEach(element => {
// naredbe za obradu
// izdvojenog elementa
})
Pod uslovom da poziv forEach stoji uz određenu iterabilnu strukturu (niz, mapa i sl), funkcija će redom proći kroz sve elemente date strukture, i pozvati "unutrašnju" lambda funkciju za svaki element.
Navedene smernice najbolje ćemo razumeti ako se poslužimo primerom u kome se metoda forEach koristi za ispis (svih) elemenata niza koji su veći od 0 ....
let niz = [ 1, 2, 3, 4, 5 ];
niz.forEach(element => {
if (element < 0) return;
console.log(element);
});
// Ekvivalentna for petlja:
for (let i = 0; i < niz.length; ++i) {
let element = niz[i];
if (element < 0) break;
console.log(element);
}
.... pri čemu se može primetiti da se elementima ne pristupa preko indeksa, već preko reference element (s tim da je u pitanju proizvoljno izabran identifikator, to jest, nije u pitanju 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 (funkcija 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 (što svakako pozdravljamo).
Javascript takođe 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 ikakav argument), uređuje niz po abecednom redosledu. *
let niz_a = [ "Maja", "Ana", "Pera", "Mika" ];
niz_a.sort(); // [ "Ana", "Maja", "Mika", "Pera"]
let niz_b = [ 2, 10, 3, 5, 1, 6, 1, 4 ];
niz_b.sort(); // [ 1, 1, 10, 2, 3, 4, 5, 6]
Da bi niz bio sortiran u nerastući poredak - prema brojčanim vrednostima (elemenata), potrebno je da se funkciji sort u svojstvu argumenta preda - funkcija.
Osnovni princip je sličan primerima iz prethodnih odeljaka, ali, tehnikalije su ponešto kompleksnije - i stoga ćemo se detaljnije pozabaviti lambda funkcijom koja se koristi u okviru funkcije sort.
U funkcijama za sortiranje nizova, često se javlja 'motiv' poređenja dva elementa niza, čije vrednosti potom treba (ili ne treba) razmeniti ....
function selectionSort(niz) {
for (let i = 0; i < niz.length - 1; ++i) {
let i_min = i;
let min = niz[i];
for (let j = i + 1; j < niz.length; ++j) {
if (niz[j] < min) { // poređenje
i_min = j;
min = niz[j];
}
}
if (i_min != i) {
let p = niz[i];
niz[i] = niz[i_min];
niz[i_min] = p;
}
}
}
.... a sam uslov za razmenu dva elementa (kao što možemo videti u gornjem primeru), zapisan je u telu funkcije - što praktično znači da se ne može menjati tokom izvršavanja programa ("tako je inače").
Shodno prethodnim uputstvima, nije teško pretpostaviti da je svrha lambda funkcije koja se funkciji sort predaje u svojstvu argumenta - odlučivanje u vezi sa tim da li će elementi niza koji se porede biti međusobno razmenjeni (ili neće).
Ako napravimo sledeći poziv (uz korišćenje pomoćne funkcije provera, koju ćemo samostalno implementirati) ....
function provera(a, b) {
return a - b;
}
let niz = [ 2, 10, 3, 5, 1, 6, 1, 4 ];
niz.sort(provera) // 1, 1, 2, 3, 4, 5, 6, 10
.... niz će biti sortiran u neopadajući poredak - 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 niz = [ 2, 10, 3, 5, 1, 6, 1, 4 ];
niz.sort(provera); // 10, 6, 5, 4, 3, 2, 1, 1
Naravno, postavlja se pitanje: kako program zapravo odlučuje da li će doći do razmene (a postavljaju se i pitanja oko toga kako dve funkcije 'sarađuju').
U funkcijama sa kojima smo se upoznali na početku poglavlja (every, some i dr), postojala je petlja koja prolazi redom kroz sve elemente, dok, kada je u pitanju funkcija sort, postoji kompleksnija struktura petlji preko kojih se elementi porede, * međutim, umesto unapred zadatih uslova (kao u primeru sa slike #28), koristi se pomoćna lambda funkcija: ukoliko funkcija za proveru vrati negativan broj (pri proveri određena dva elementa) - doći će do razmene, a ukoliko funkcija vrati nenegativan broj - neće doći do razmene.
U prvom slučaju (iz gornjih primera), do razmene dolazi ako je argument b veći, dok u drugom slučaju do razmene dolazi ukoliko je veći argument a.
Preko lambda notacije, pozivi se mogu dodatno uprostiti:
niz.sort((a, b) => a - b) // uređivanje niza u neopadajući poredak
niz.sort((a, b) => b - a) // uređivanje niza u nerastući poredak
Da se podsetimo (još jednom/"za svaki slučaj"): sve što smo naveli i prikazali u vezi sa povratnim pozivima i lambda izrazima u ugrađenoj funkciji sort, moguće je izvesti samo zato što je funkcija sort projektovana tako da može na poseban način pokretati (druge) funkcije koje se predaju u svojstvu argumenata, a pre nego što se 'odjavimo', navešćemo i jednu napomenu preko koje ćemo 'postaviti stvari u perspektivu' (pre svega zarad čitalaca koji se tek upoznaju sa tematikom).
Naime, iako su kodovi koje smo prikazali prilično intuitivni (i mnogi čitaoci su verovatno već stekli prilično jasnu predstavu o tome kako se lambda notacija može koristiti), obim gradiva koje je prikazan u članku o funkcijama za obavljanje operacija sa nizovima u JS-u, ni iz daleka nije dovoljan za postizanje pravog razumevanja funkcija povratnog poziva i lambda notacije, i stoga preporučujemo da detaljnije proučite članak koji smo posvetili navedenim temama (naravno, onda kada dođe vreme). :)
Kratak rezime ....
Na kraju, može se (ipak :)) zaključiti da obavljanje operacija sa nizovima u JS-u nije nikakav "bauk".
Biće potrebno vreme (da 'pohvatate konce'), biće potrebno malo više pažnje (u odnosu na tipizirane jezike, u kojima postoje određeni mehanizmi koji sprečavaju 'nepodopštine i marifetluke', kao što je pokušaj dodavanja znakovne niske u niz celobrojnih vrednosti i sl), ali - ukoliko se potrudite - verujemo da ćete se u svemu snaći sasvim uspešno, i dobri rezultati neće izostati.
Sledeći članak o JS-u, posvetićemo implementaciji čvorova i struktura podataka ....