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 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 prethodnog pasusa; nismo zaboravili), i mora se priznati da u većini situacija u praksi (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, 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 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), valja 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.
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 koji 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
):
Niz je prazan, ali, svakako se u daljem toku izvršavanja programa mogu dodavati novi elementi.
Inicijalizacija niza
Pored inicijalizacije praznog niza (kakvu smo već videli), često * se javlja i inicijalizacija/deklaracija koja podrazumeva zadavanje (nekoliko) elemenata:
Nizovi se takođe mogu inicijalizovati i preko funkcija (čija je povratna vrednost referenca na početak niza).
Š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 - ukoliko dođe do greške - treba se postarati da funkcija 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 (navođenje indeksa u velikoj zagradi): *
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), može se očitati preko svojstva length
:
Sada možemo (recimo) napraviti i for
petlju koja pristupa svim elementima:
Dodavanje elementa na kraj niza (push)
Za dodavanje elementa na kraj niza, koristi se funkcija push
:
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, 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
:
Element sa početka niza, uklanja se preko funkcije shift
:
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:
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
.
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: indeks prvog elementa koji se uklanja, i ukupan broj elemenata koje je potrebno ukloniti.
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đ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.
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:
Spajanje niza u nisku znakova (join)
Ukoliko je potrebno niz elemenata spojiti u nisku, može se koristiti funkcija join
:
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 pojavljuje se u okviru rezultujuće niske, 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
, 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':
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:
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 rad sa nizovima
Funkcije koje smo do sada prikazali 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 ipak je u pitanju relativno jednostavan način zapisa koji većina programera (čak i mlađih i/ili neiskusnijih), može lako prihvatiti intuitivno.
every
Naveli smo već da je sintaksa funkcija višeg reda relativno jednostavna (tipično), pa tako sledeći kod ....
.... vraća vrednost true
pod uslovom da su svi elementi niza veći od 1 (dok je u suprotnom povratna vrednost false
).
Međutim, budući da (ipak) nisu u pitanju trivijalne ideje, potrudićemo se da odmah pojasnimo kako se navedeni kod interpretira.
U konkretnom primeru (i inače), pre svega je potrebno razumeti da lambda notacija * ne funkcioniše "sama od sebe", već - zato što je konkretna funkcija koja se koristi (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 ispituje određeni uslov.
some
Naizgled vrlo slično prethodnom primeru, sledeći kod ....
.... 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:
Pokretanjem skripte, nastaje novi niz sa vrednostima [ 1, 4, 9, 16, 25 ]
.
Nove vrednosti predstavljaju kvadrate vrednosti iz ulaznog niza, a funkcija preko koje se definišu nove vrednosti, i ovoga puta se predaje kao argument (u zagradi).
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:
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, može se koristiti sledeći obrazac ....
.... koji lakše možemo razumeti preko primera u kome se metoda forEach
koristi za ispis (svih) elemenata niza ....
.... 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 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. *
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 prethodnim primerima, 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 ....
.... 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):
.... 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:
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 (every
i some
), 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 #25), 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:
Da se podsetimo (još jednom/"za svaki slučaj"): 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 funkcija projektovana tako da može na poseban način pokretati (druge) funkcije koje se predaju u svojstvu argumenata.
Pre nego što se 'odjavimo', navedimo i jednu napomenu preko koje ćemo 'postaviti stvari u perspektivu' (pre svega zarad čitalaca koji se tek upoznaju sa tematikom): 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 rad 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 rad 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 programere da u niz celobrojnih vrednosti dodaju niske 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 ....