Uvod
JavaScript je specifičan programski jezik i u poredjenju sa drugim jezicima ponekad probleme rešava na drugačiji način od očekivanog, ali to ne znači da je taj način i pogrešan. Iza svake “čudne” odluke vezane za sintaksu jezika stoji dobar razlog zbog čega je tim koji radi na razvoju JavaScript-a baš nju izabrao. Da bi se razumele specifičnosti jezika potrebno je temeljno proučiti sintaksu i principe rada jezika. Postoji jedan manji broj nelogičnosti koje i dalje ne mogu tako lako da “svarim”, ali niko nije savršen pa tako ni JavaScript. Smatra se da programeri koji dodju u kontakt sa JavaScript-om kao drugim jezikom imaju malo više problema da se prilagode, jer očekuju iste principe kao u svom omiljenom programskom jeziku ali i ih ovde ne nalaze. U ovom članku sam sakupio potencijalne (ne)iznudjene greške koje može da napravi običan JavaScript smrtnik. Članak će se redovno ažurirati kako budem grešio 🙂
Čudni rezultati pri sabiranju decimalnih brojeva
1 2 3 4 5 6 7 8 9 |
// Neke vrednosti su ok: 0.2 + 0.3 = 0.5 0.2 + 0.5 = 0.7 0.1 + 0.4 = 0.5 // Ali ove vrednosti su čudne: 0.1 + 0.2 = 0.300000004 0.2 + 0.4 = 0.6000000000000001 0.1 + 0.7 = 0.7999999999999999 |
Ovaj problem se dešava jer u nekim slučajevima decimalni brojevi ne mogu da se prikažu kao binarna frakcija (kao što npr. 1/3 ne može da se prikaže kao decimalna frakcija). Ovakvo ponašenje može da bude problem u izrazima, kao npr.:
1 2 3 |
if (rezultat == očekivaniRezultat) { // Uradi nešo } |
Rešenje
Ukoliko se traži odredjena tačnost može da se koristi naredni postupak:
1 2 3 4 |
var tacnost = 0.00001; if (abs(rezultat - očekivaniRezultat) < tacnost){ // Neki kod } |
Ili
1 2 3 4 |
var rezultat = 0.2 + 0.1; if (rezultat.toFixed(4) == očekivaniRezultat){ // Neki kod } |
Implicitna promena tipa
Implicitna (prikrivena) konverzija tipova se odnosi na konverzije koje nisu očigledne, a izvšava ih JavaScript engine u hodu kao sporedni efekat nekih drugih radnji. Implicitna konverzija se najčešće javlja kada se vrednost nekog tipa koristi na način koji automatski prouzrokuje njenu konverziju.
Asocijativnost operacija i konverzija logičke vrednosti u broj
Gledano sa matematičke strane naredni snippet je po svemu netačan, medjutim ako se zna da je znak manje “<" u JavaScript-u operator komparacije sa svojim setom pravila postaje jasno zašto je rezultat TRUE:
1 |
3 < 2 < 1 // Vraća TRUE |
Objašnjenje:
Asocijativnost operatora “<" je s’ leva na desno, pa izraz nakon grupisanja izgleda ovako:
1 |
(3 < 2) < 1 |
Rešavanjem prve operacije dobija se logička vrednost FALSE, pa izraz izgleda
1 |
FALSE < 1 |
Pri poredjenju dva elementa različitog tipa dolazi do implicitne konverzije FALSE u broj “0” nakon čega izraz izgleda:
1 |
0 < 1 // Vraća: TRUE |
Više o operatorima pogledajte u članku “JavaScript operatori”
Implicitna konverzija u logički tip pri poredjenju
U slučaju kada se sa logičkim operatorima (&& ili ||) porede operandi koji nisu logičkog tipa, zbog ispitivanja uslova operatora dolazi do implicitne konverzija operanada u logički tip. Medjutim operatori && ili || nakon razrešenja uslova ne vraćaju logičku vrednost, već vraćaju vrednost jednog od dva operanda. Razultat uslova se odredjuje prema sledećim pravilima:
- Za operator || nakon konverzije “ne logičkog” tipa u logički pri razrešenju uslova važi pravilo da ukoliko je rezultat uslova operatora:
- TRUE vraća vrednost prvog operanda
- FALSE vraća vrednost drugog operanda
- Za operator && nakon konverzije “ne logičkog” tipa u logički pri razrešenju uslova važi pravilo da ukoliko je rezultat uslova operatora:
- FALSE vraća vrednost prvog operanda.
- TRUE vraća vrednost drugog operanda
1 2 3 4 5 |
var a = 42; var b = "foo"; var c = [1,2,3]; a && b || c // Rezultat je : "foo" |
Objašnjenje rezultata primera:
Uvažavajući prioritete operatora koji su korišćeni, logičko “i” ima prednost nad logičkim “ili”, pa prethodni izraz može da se konvertuje u:
1 |
(a && b) || c |
Za odredjivanje uslova u zagradi se prvo privremeno vrši implicitna konverzija tipova u logički tip, nakon toga se izvrši proracun sa operandom && nad dobijenim logičkim tipovima:
1 |
(TRUE && TRUE) || c |
Rešenje uslova (TRUE && TRUE) je TRUE, pa stoga prema pravilu ukoliko je rezultat && TRUE operator vraća vrednost drugog operanda (u ovom slučaju “foo”), pa sredjeni izraz ovako izgleda:
1 |
"foo" || [1,2,3] |
Za odredjivanje uslova se prvo privremeno vrši implicitna konverzija tipova u logički tip, nakon toga se izvrši proracun sa operandom || nad dobijenim logičkim tipovima:
1 |
TRUE || TRUE |
Rešenje ovog uslova je TRUE, nakon čega operator || prema pravilu vraća vrednost prvog operanda (a to je “foo”).
Više o operatorima pogledajte u članku “JavaScript operatori”
Implicitna konverzija Objekta u string
Ukoliko želimo da ištampamo u konzoli neki objekat zajedno sa pratećim tekstom, dobijamo neočekivani rezultat:
1 2 |
var obj = {a:1, b:2}; console.log('Objekat je: ' + obj); // Vraća: "Objekat je: [object Object]" |
Korišćenje operatora + sa operandima koji nisu brojevi nije sabiranje već konkatenacija string-ova. Konkatenacija je privilegija sting-ova, stoga rezultat iz prethodnog primera je posledica implicitne (prikrivene) konverzije objekta u string.
1 2 3 4 5 |
var obj1 = {a : 1}; console.log(obj1.toString()); // Vraca: "[object Object]" var obj2 = {}; console.log(obj2.toString()); // Vraca: "[object Object]" |
Iz prethodnog se zaključuje da svaka konverzija objekta u string vraća “[object Object]”
Rešenje:
Da bi se objekat “lepo” ištampao potrebno je da prvo JavaScript objekat prebacimo u string sa JSON.stringify() metodom:
1 2 3 |
var obj = {a:1, b:2}; var objString = JSON.stringify(obj); console.log('Objekat je: ' + objString); // Vraća: "Objekat je: {"a":1,"b":2}" |
Ukoliko na prvom snippet-u umesto + stavimo zarez dobićemo željeni rezultat, jer operator zarez izvršava postavljeni task za sve zarezom odvojene parametre a ne kastuje objekat u string:
1 2 3 |
var obj = {a:1, b:2}; console.log(obj); console.log('Objekat je : ' , obj); // Vraća: Objekat je : Object {a: 1, b: 2} |
Implicitna konverzija niz-a u string
Kada zbog konkatenacije dolazi do implicitne konverzije niza u string, dobijaju malo drugačiji rezultati:
1 2 3 4 5 6 7 8 9 |
var niz1 = [1, 2, 3]; console.log("Niz sadrži: "+ niz1); // Vraća "Niz sadrži: 1,2,3" console.log(niz1.toString()); // Vraca: "1,2,3" var niz2 = []; console.log(niz2.toString()); // Vraca: "" var niz3 = [1, {a:2}, 3]; console.log(niz3.toString()); // Vraca: "1,[object Object],3" |
Više o ovome pročitajte u članku Konverzija tipova u JavaScript-u”
Striktno poredjenje objekata?
Poredjenjenje objekata sa “===” operatorom uvek vraća FALSE jer se objekti prosledjuju prema referenci u memoriji. Na sledećem primeru se striktno porede dva objekta sa istim vrednostima, medjutim pošto su te vrednosti sačuvane na dva različita mesta u memoriji, poredjenje je uvek FALSE:
1 2 3 |
{[1,2,3] === [1,2,3] // false {a: 1} === {a: 1} // false {} === {} // false |
Pri dodeljivanju jednog objekta nekoj promenjivoj, prosledjuje se referenca na mesto gde je sačuvan objekat u memoriji. Objekti (u to spadaju i nizovi) se čuvaju u tipu memorije zvanom “heap”.
1 2 3 4 5 |
var c = {a:1}; var d = c; // ukoliko promeimo vrednost "c" c = {a :2}; console.log(d); // Vraća: {a:2} jel ukazuje na isto mesto u memoriji. |
Ipak treba znati da naredni kod vraća TRUE, jer oba ukazuju na isto mesto u memeriji:
1 2 3 |
var e = {a: 1}; var f = e; e === f; // Vraća: TRUE |
Više o podacima sa referentnim vrednostima pogledajte u članku “Tipovi podataka u JavaScriptu”
Type of Null!?
Jedna od neobičnih i čudnih stvari vezanih za sintaksu jezika je činjenica da je null tipa object!!!
1 |
typeof null ==="object" // TRUE |
Kontrola postojanja objekta je problematičan task, jer kondicioni uslov obj !== “undefined” daje lažno pozitivne rezultate za null:
1 2 3 |
if (obj !== "undefined"){ alert ("Objekat postoji") } |
Iz prethodnog razloga se najčešće koristi metoda typeof koja vraća string sa kojim definiše tip podataka.
1 2 3 |
if (typeof obj !== "undefined"){ alert ("Objekat postoji") } |
Medjutim prethodna provera nije dovoljna jer objekat može imati dodeljenu vrednost null:
1 2 3 |
if (obj !== null && typeof obj !== "undefined"){ alert ("Objekat postoji") } |
Ali ni prethodni kod nije dovoljno dobar jer ukoliko objekat undefined onda nije null, stoga je bolje da se prvo pita da li je različit od undefined:
1 2 3 |
if (typeof obj !== "undefined" && obj !== null){ alert ("Objekat postoji") } |
Ugradjeni objekti koji liče na primitive
U JavaScript-u pored tipa “object” postoje još 6 “prostih primarnih” tipova koji nisu objekat: string, number, boolean, undefined, null i symbol. Ali često zbrku pravi činjenica da pored primitivnih tipova postoje i ugradjeni objekti čija su imena ista kao kod prostih primitiva stim što nazivi počinju sa velikim slovom: String, Number, Boolean, Function, Array.
Često dolazi do implicitne konverzije prostog primarnog tipa u njegov “parnjak” objekat pa dolazi do neočekivanih rezultata pri striktnom poredjenju. Na sledećem primeru je prikazano da striktno poredjenje vraća false:
1 2 3 4 5 6 7 8 |
(function(n) { return n === new Number(n); })(10); // Vraca: FALSE jer se poredi number i objekat. ili (function(x) { return new String(x) === x; })('a'); //Vraca: FALSE jer se poredi string i objekat |
U prethodnom primeru snippet-i vraćaju FALSE, jer se pri korišćenju rezervisane reči new se kreira novi objekat.
Više o ovome pročitajte u članku “Tipovi podataka u JavaScriptu” pod sekcijom “Da li je sve u JavaScript-u objekat?”
Striktna komparacija unutar Switch() izraza
1 2 3 4 5 |
var myVar = 5; switch(myVar){ case '5': alert("hi"); // Nikada se neće aktivirati } |
Prethodni snippet nikada neće aktivirati alert, jer switch izraz traži da se podudara i tip podatka. Ali ukoliko prvo kastujemo promenjivu u string naredni snippet će davati ispravne rezultate.
1 2 3 4 5 |
var myVar = 5; switch(myVar.toString()){ case '5': alert("hi"); } |
Specifičnost metode replace()
Potrebno je znati da metoda replace() utiče samo na prvi element koji pronadje:
1 2 3 |
var a = "bob ili bob"; var rec = "bob"; alert(a.replace(rec, "lol")); // Vraća" lol ili bob |
Stoga ukoliko želimo da primenimo zamenu string-a na sve tražene elemente, potrebno je da koristimo regular expression i to globalno:
1 2 3 |
var a = "bob ili bob"; var patern = /b/ig; alert(a.replace(patern, "l")); //Vraća: lol ili lol |
Problem numerisanja meseci u Date objektu
Meseci u JS objektu “date” počinje da se numerišu od nule, za razliku od godjine i dana koji počinju da se nabrajaju od broja jedan.
1 2 |
new Date (2016, 05, 20); // Vraća 20-ti jun 2016 new Date (2016, 05, 31); // Vraća 01. jul 2016 (jer jun ima 30 dana pa se prelije u jul) |
Definisanje niza sa jednim argumentom
Ukoliko se pri deklarisanju niza stavi samo jedna vrednost, JS smatra da je to “lenght” niza. Zbog toga JavaScript pravi niz takve dužine čiji članovi još nisu definisani.
1 2 |
new Array(3); //Vraća niz [undefined, undefined, undefined] new Array(1, 2, Array(3)); //Vraća niz [1, 2, [undefined, undefined, undefined]] |
Problem sortiranja niza brojeva sa metodom sort()
Metoda sort() je inicijalno namenjena za sortiranje niza stringova, stoga pri sortiranju niza brojeva daje pogrešne rezultate. Za korišćenje sa brojevima je potrebno dodati funkciju poredjenja.
1 2 |
//Sortiranje brojeva kao što se sortiraju stringovi daje pogrešan rezultat: [10,1, 5].sort() // [1, 10, 5] |
Rešenje
Za korišćenje sa brojevima je potrebno dodati funkciju poredjenja.
1 2 3 4 5 |
// Sortiranje brojeva sa ES5 nekiNiz = nekiNiz.sort(function (a, b) { return a - b; }); // Sortiranje brojeva sa ES2015 nekiNiz = nekiNiz.sort((a, b) => a - b); |
Argumenti funkcije
Lenght funkcije
Svojstvo lenght kod funkcija vraća broj definisanih argumenata (ovde su 2kom: a,b) u funkciji (ne prosledjenjih ili korišćenih argumenata).
1 |
(function nekaFunkcija(a,b){}).lenght // Vraća 2 |
Specifičnost “arguments” objekta
Arguments je “Array like” objekat stoga za razliku od “običnog” objekta ima ima svojstvo lenght. Operator delete se koristi da briše svojstva objekta, u narednom primeru zelimo da obrišemo prvo svojstvo objekta arguments.
1 2 3 4 5 6 |
var result = (function(a, b, c) { delete arguments[0]; return arguments.length; })(5,7,9); console.log(result); // 3 |
Medjutim iako je operator obrisao prvo svojstvo objekta “arguments” to ne utiče na vrednost koju vraća metoda lenght(), jer metoda lenght vraća broj prosledjenih argumenata funkciji (u ovom slučaju su 3 kom: 5, 7, 9).
Zaklanjanje promenjive (shadowing)
1 2 3 4 5 6 |
var plata = "1000"; (function () { console.log("Pocetna plata je " + plata); var plata = "5000"; console.log("Nova plata je " + plata); })(); // Vraća: "Pocetna plata je undefined" i "Nova plata je 5000" |
Činjenica da je promenjiva iz spoljnog scope-a uvek dostupna kodu unutar unutrašnjeg scope, može da nas “zavede” da pomislimo da će se štampati “pocetna je 1000”. Medjutim ovde dolazi do zaklanjanja promenjive (eng. variable shadowing). Kada JavaScript engine traži vrednost neke promenjive on prvo pogleda u najbližoj oblasti definisanosti, a pošto je promenjiva definisana u sklopu funkcije, on je tu i nalazi (dalje i ne traži pa nikada ne dodje do spoljne promenjive). Pored “zaklanjanja promenjive” dešava se i proces hoisting-a promenjive unutar funkcije. Ovakvo rešenje se lako objašnjava u narednom snippet-u, gde je prikazan kod trenutku parsiranja JavaScript-a.
1 2 3 4 5 6 7 |
var plata = "1000"; (function () { var plata; // undefined; console.log("Pocetna plata je " + plata); plata = "5000"; console.log("Nova plata je " + plata); })(); |
Vrednost “this” u “izvučenoj” metodi objekta
Metoda je samo svojstvo objekta koje ima referencu na neku funkciju. Kada metodu objekta dodelimo nekoj promenjivoj mi smo promenjivoj dodelili referencu na funkciju. Stoga pozivanjem te promenjive mi ne pozivamo metodu objekta nego običnu funkciju iz globalnog domena. Ključna reč THIS unutar funkcije koja se poziva iz globalnog scope prema pravilima za THIS ukazuje na globalni objekat (window).
Primer
1 2 3 4 5 6 7 8 9 10 11 |
var osoba = { ime:"Pera", prezime: "Peric", punoIme: function () { return this.ime + " " + this.prezime; } } osoba.punoIme(); // Vraca: Pera Perić var prikazPunogImena = osoba.punoIme // dodeljujemo promenjivoj referencu na funkciju prikazPunogImena(); // vraca: undefined undefined |
Objašnjenje primera
Pošto metoda osoba.punoIme ima referencu na anonimnu funkciju možemo je ubaciti njeno mesto pa prethodni primer sa gledišta this prelazi u sledeći kod:
1 2 3 4 |
var prikazPunogImena = function () { return this.ime + " " + this.prezime; } prikazPunogImena(); |
Iz priloženog se vidi da se poziva funkcija prikazPunogImena(), a ne metoda objekta. Stoga se koristi “podrazumevano pravilo” kada this ukazuje na globalni objekat window koji nema svojstvo window.ime i window.prezime pa vraća undefined, undefined.
Rešenje problema
Ovaj problem se rešava sa bind() metodom, kojom ćemo explicitno povezati this iz funkcije sa objektom “osoba”, nakon čega više nije bitno odakle se funkcija poziva.
1 2 3 4 5 6 7 8 9 |
var osoba = { ime:"Pera", prezime: "Peric", punoIme: function () { return this.ime + " " + this.prezime; } } var prikazPunogImena = osoba.punoIme.bind(osoba) // sada smo vezali this za objekat "osoba" prikazPunogImena(); // Pera Peric |
Više o ovome pročitajte u članku “This u JavaScript-u”
Problem callback funkcije u loop-u
Neočekivano ponašanje callback funkcije u petlji možemo zahvaliti tome što se ona ne poziva odmah dok petlja vrti, već se poziva uvek sa “zakašnjenjem” kada je petlja već izvrtela i došla do poslednje vrednosti brojača. Iz tog razloga callback funkcija će uvek koristiti poslednju vrednost brojača.
Loop i callback funkcija kod setTimeout() metode
1 2 3 4 5 |
for (var i = 0; i < 5; i++){ setTimeout(function(){ console.log(i); }, 1000); } // Vraća: 5 5 5 5 5 |
Opis procesa
Pozivanje metode setTimeout dok se vrti petlja
Po startovanju petlje početna vrednost promenjive “i=0”, nakon čega se poziva funkcija setTimeout(). Funkcija setTimeout() će tek za 10s pozvati callback funkciju. Dok se čeka na pozivanje callback funkcije, petlja nastavlja da “vrti” i sada je “i=1”, odmah zatim se poziva se funkcija setTimeout() koja će za 10s opet pozvati callback funkciju, a dok se čeka na njeno izvršenje, petlja vrti dalje i sada je “i=2”. Postupak se ponavlja sve dok “i” ne dobije vrednost 5. Ovaj ceo prethodno pomenuti postupak je izvršen veoma kratkom periodu (mereno milisekundama).
Pozivanje callback funkcije po završetku petlje
Nakon prvih 10sec petlja već “izvrtela” promenjivu “i” i “poziva” se (invoke) prva callback funkcija koja stoga uzima vrednost promenjive “i” a to je 5. Naredna callback funkcija za se aktivira za dodatnih 10sec, i koristi istu promenjivu pa je rezultat je isti…
Rešenje problema:
Ovaj problem možemo da rešimo ako callback funkciju pozovemo pri svakom loop-u, da bi ona “zgrabila” vrednost promenjive “i” u tom trenutku. Na ovaj način pravimo za svaki prolaz kroz petlju novu funkciju, koja zahvaljujući karakteristikama closure u stanju da “pamti” dodeljenu vrednost promenjive “i” u tom krugu.
I način:
U ovom primeru pozivamo funkciju pri svakom krugu petlje uz pomoć IIFE, i tako joj obezbedjujemo jedinstvene vrednosti za svaki loop.
1 2 3 4 5 6 7 |
for (var i = 0; i < 5; i++){ (function(i){ setTimeout(function(){ console.log(i); }, 1000); })(i); } // Vraća: 0 1 2 3 4 |
II način:
Ovaj način koristi osobinu ES6 ključne reči let koja definiše novu promenjivu “i” u svakoj iteraciji petlje:
1 2 3 4 5 |
for (let i = 0; i < 5; i++){ setTimeout(function(){ console.log(i); }, 1000); } // Vraća: 0 1 2 3 4 |
III način:
U ovom primeru callback funkciju izdvajamo da bi je u sintaksi setTimeout() funkcije mogli pozvati (invoke) pri svakom prolasku petlje. Closure će zapamtiti tu vrednost u trenutku pozivanja i na nju neće uticati kasnija promena vrednosti promenjive tj. “redeklarisanje promenjive”
1 2 3 4 5 6 7 8 9 |
function nekaFunkcija (i){ return function(){ console.log(i); }; }; for (var i = 0; i < 5; i++){ setTimeout(nekaFunkcija(i), 10000); } |
Loop i callback funkcija kod addEventListener()
Na sledećem primeru se javlja sličan problem kao kod petlje i funkcije setTimeout():
See the Pen VWPNPq by Web programiranje (@chos) on CodePen.
Kao u svakom standardnoj petlji na svaki element je dodat eventListener i petlja se završila. U trenutku kad se klikne na neko dugme petlja je već “izvrtela” i koje god dugme da se izabere alert će da bude jednak.
Rešenje
Jedno od mogućih rešenja je da se stavi ceo kod u IIFE koja će se pozivati pri svakom krugu a clousure će da pamtiti prosledjene vrednost:
See the Pen gRgyOy by Web programiranje (@chos) on CodePen.
Više o ovome možete pročitati u članku “Javascript Clousure” pod sekcijom “Rešavanje problema sa “this” kod callback funkcije u petlji”
Poštovani,
pod naslovom “Zaklanjanje promenjive (shadowing)” u prvom code snippetu trebalo bi da stoji “var plata = 5000;” umesto “plata = 5000;” kako bi došlo do shadowing-a. Ovako se samo reinicijalizuje globalna varijabla “plata”.
Hvala puno, ispravljeno!