6 JavaScript WTFs și ce poți învăța din ele

Sunt convins că în parcursul tău de JavaScript developer te-ai lovit de cel puțin o dată (pe zi) de eroarea “undefined” is not a function, sau de “the type of the NaN is actually a number”. Ei bine, câteodată limbajul JavaScript vrea să te pună sub presiune. 

În acest articol, vei face parte din aventura amuzantă, dar și întunecată a acestui frumos limbaj de programare. Hai să începem!

1. Min > Max

Math.min() > Math.max() //adevarat
Explicație

În primul rând, haideți să definim anumite aspecte:

  • Math este un obiect încorporat (built-in) care are proprietăți și metode legat de funcții matematice. Acesta nu este un obiect funcțional.
  • Funcția statică Math.max() returnează numărul cu cea mai mare valoare care a fost pasată ca și parametru, sau NaN dacă orice parametru nu este un număr, iar acesta nu poate să fie convertit într-unul.  

Perfect, acum știm ce reprezintă obiectul Math în JavaScript și ce face funcția statică .max(). Asemănător, funcția .min() va face operația inversă celei descrise mai sus. Până acum, instinctul ne-ar spune că Math.max() ar trebui să returneze Number.MAX_VALUE dacă niciun parametru nu este trimis.

Totuși, această asumptie ar fi eronată. Imaginează-ți că ar trebui să implementezi o funcție care găsește valoarea maximă dintr-un șir. Cea mai ușoară cale de a face asta este să parcurgi întregul șir, să compari elementele acestuia și să reții valoarea maximă. Ideea este că acea variabilă care stochează maximul va trebui inițializată cu o valoare extrem de mică, cea mai mică. 

Acum s-ar putea să te gândești că cea mai mică valoare în JavaScript este Number.MIN_VALUE(5e-324) și da, ai dreptate. Dar JavaScript ți-a pregătit o surpriză în acest caz, iar aceasta este valoarea Infinity.

“The global Infinity property is a numeric value representing infinity.”

Și în sfârșit, mai jos găsești definiția completă a funcției .max()

Math.max() returns the largest of the given numbers. If any one or more of the parameters cannot be converted into a number, NaN is returned. The result is -Infinity if no parameters are provided.

Math.min() > Math.max() -> Infinity > -Infinity //adevarat

Ce am învățat:

  1. Ce reprezintă obiectul Math
  2. Cum funcționează funcțiile min() și max()
  3. Despre obiectul Infinity în JavaScript

2. 0.1 + 0.2  = ?

Poți spune că asta e prea ușor, 0.1 + 0.2 = 0.3, nu? Nu și în JavaScript! (sau Java, sau C++, sau C#, sau… ai prins ideea).

0.1 + 0.2 === 0.3 //fals
Explicație

Cum este acest lucru posibil? Înainte de a te gândi la matematica de bază pe care ai invățat-o, lasă-ma să-ți prezint Virgula Mobilă (Floating point Math).

“Computers can only natively store integers, so they need some way of representing decimal numbers. This representation comes with some degree of inaccuracy.”

https://0.30000000000000004.com/

Acest subiect este unul complex și necesită câteva ore serioase investite în aprofundarea lui. Totuși, voi încerca să-l simplific pentru această situație particulară.  

În cazul sistemului cu bază 10 singurele fracții care pot fi exprimate într-un mod curat sunt cele cu factorul prim ca bază (½, ¼, ⅕ , etc.). În comparație ⅓ are zecimale în perioadă (0,33333…). Astfel, dacă luăm această informație și o aplicăm în cazul sistemul în bază 2, fracțiile curate vor fi ½, ¼  și ⅛, în timp ce ⅕ și 1/10 au zecimale în perioadă. Acest lucru cauzează anumite “resturi” în exemplul descris mai sus.

0.1 + 0.2 === 0.30000000000000004 //adevarat

 “When you do the math on these repeating decimals, you end up with leftovers which carry over when you convert the computer’s base 2 (binary) number into a more human-readable base 10 number.

https://0.30000000000000004.com/

Ce am învățat:

  1. Virgula mobilă – prezentare generală
  2. Acest concept se aplică în cazul majorității limbajelor de programare

3. baNaNa 

Super! După problema matematică mai dificilă discutată mai sus, hai să facem ceva amuzant!

"b" + "a" + +"a" + "a" -> baNaNa
Explicație:

Spre deosebire de celelalte două WTFs, acesta este puțin mai simplu. Acest lucru se datorează faptului că ai 75% din problemă rezolvată deja. Ce ne rămâne este să clarificăm un mic aspect: ce va returna ++”a”.

Sintaxa este corectă pentru că al doilea “+” nu reprezintă un operator de adăugare ci un operator unar (unary operator). 

The unary + operator converts its operand to Number type. Any unsupported values will be evaluated to NaN.

Deci expresia noastră va fi precum mai jos, deoarece “a” nu poate să fie convertit într-un număr.

"b" + "a" + NaN + "a" -> baNaNa

Pentru a concluziona trebuie să clarificăm încă o piesă de pe tabla de șah. Ce va returna String + String + NaN + String? Cum se va comporta operatorul de adăugare?

The addition operator either performs string concatenation or numeric addition.

Astfel sunt două tipuri de adăugare ce pot apărea, concatenarea stringurilor sau adunarea numerică, fix în această ordine. Modul în care algoritmul va funcționa este următorul:

  • Transformă operanzii în primitive folosind funcția ToPrimitive().
  • Dacă unul dintre operanzi este String, atunci îl va transforma pe celălalt într-un String și va executa concatenarea stringurilor. Altfel, convertește ambii operanzii în numere și execută adunarea numerică. 
"b" + "a"-> "ba"
"ba" + NaN -> "ba" + "NaN" -> "baNaN"
"baNaN" + "a" -> "baNaNa"

Ce am învățat:

  1. Ce este un operator unar
  2. Algoritmul operatorului de adăugare
  3. Funcția ToPrimitive() și un caz de folosire a acesteia

4. Inițializare înainte de declarare?

Să luăm codul de mai jos ca exemplu:

mesaj = "nu uita sa dai un share!";
console.log(articolPromovare("Cititorule"));
function articolPromovare(nume) {
    return `${nume}, ${mesaj}`;
};
var mesaj;

Ce va fi afișat în consolă? O eroare de tip “ReferenceError” pentru că variabila “message” nu este definită? Sau poate șirul de caractere “Cititor, undefined”? Nu, trebuie să fie TypeError, articolPromovare nu este o funcție.

Explicație:

Din fericire, rezultatul va fi exact cel pe care îl dorim: “Cititor, nu uita sa dai un share!”. Dar oare de ce? Comportamentul “Hoisting” al JavaScript este responsabil pentru acest lucru.

Hoisting is JavaScript’s default behavior of moving declarations to the top.

Observație: Acest lucru este disponibil doar pentru variabilele definite utilizând keyword-ul var și funcțiile declarate.

Folosind această informație putem afirma că după compilare codul nostru va arăta astfel:

function articolPromovare(nume) {
    return `${nume}, ${mesaj}`;
};
var mesaj;
mesaj = "nu uita sa dai un share!";
console.log(articolPromovare("Cititorule"));

Hai să o luăm pas cu pas. Funcția articolPromovare() este prima deoarece declararea funcțiilor sunt primele elemente ce apar în cod. Acestea sunt urmate de declararea variabilelor.

De asemenea, nicio eroare nu apare pentru că funcția are valoarea corectă în momentul în care aceasta va fi apelată. Variabila a fost, de asemenea, declarată și inițializată.

Pentru a înlătura orice urmă de confuzie care ar putea aparea, voi menționa diferența dintre “declared functions” și “expression functions”. Mai jos este un exemplu ce le cuprinde pe amândouă.

function promovareArticolDeclarat(nume) {
    return `${nume}, ${mesaj}`;
};
var expresiePromovareArticol= function(nume) {
    return `${nume}, ${mesaj}`;

Iar după compilare:

function promovareArticolDeclarat(nume) {
    return `${nume}, ${mesaj}`;
};
var expresiePromovareArticol; // doar procesul de declarare a fost "hoisted"
expresiePromovareArticol= function(nume) {
    return `${nume}, ${mesaj}`;

Ce am învățat:

  1. Ce este termenul Hoisting
  2. “declared functions” vs “expression functions”

5. typeof NaN == ‘number’

Explicație:

Asta ar putea să pară puțin ciudat mai ales din punct de vedere lexical, “Niciun număr nu este un număr”. Totuși, nu te îngrijora, totul va avea sens. În primul hai să aruncăm o privire pe următoarea definiție:

“The global NaN property is a value representing Not-A-Number.

La prima vedere, ar putea să pară o definiție simplă pentru NaN, dar adevăratul truc stă în cuvântul ”global”. Contrar primului instinct, NaN nu este un keyword (precum null, if, var, etc.), ci o proprietate globală. Dar oare ce obiect global poate să includă această proprietate? Da, ai ghicit, este obiectul Number.


typeof NaN == 'number' -> typeof Number.NaN == 'number' //true

Number.MIN_VALUE The smallest positive representable number – that is, the positive number closest to zero (without actually being zero).
Number.NaN Special “not a number” value.

Poate te vei întreba de ce am extras și proprietatea MIN_VALUE. Motivul este faptul că va fi mult mai clar de ce compilatorul JavaScript nu face nicio diferență dintre MIN_VALUE și proprietatea NaN, iar de aceea tipul ambelor proprietăți este number.

Ce am învățat:

  1. NaN nu este un keyword, ci o proprietate
  2. Cum funcționează operatorul type of în acest caz

6. Array.prototype.sort()

Subiectul de discuție al ultimului WTF este legat de comportamentul metodei sort(), fără niciun parametru trimis.

[32, 3, 6].sort() //[3, 32, 6]
Explicație:

După cum se vede, ce am dorit să facem nu a funcționat așa cum ne-am așteptat. De ce valorile sunt în această ordine? Hai să mai vedem câteva exemple și să cercetăm mai departe.

[32, 3, true, 6].sort(); //[3, 32, 6, true]
[32, 3, true, 6, NaN].sort(); //[3, 32, 6, NaN, true]
[32, 3, true, 6, NaN, undefined].sort(); //[3, 32, 6, NaN, true, undefined]

Te-ai prins? Da, algoritmul implicit transformă fiecare valoare într-un string iar apoi le sortează în consecință.

Pentru a obține rezultatul dorit, metoda sort() va avea nevoie de o funcție de comparare ca și parametru. Această funcție primește doi parametri și returnează un număr care descriere relația dintre ele.

The notation a < b means comparefn(a, b) < 0; a = b means comparefn(a, b) = 0 (of either sign); and a > b means comparefn(a, b) > 0.

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort

Mai jos găsești un exemplu ce folosește un șir de obiecte. Algoritmul de sortare este bazat pe proprietatea „vârstă” a fiecărui utilizator.

let utilizatori = [
    {
        nume: "Andrei",
        varsta: 25
    },
    {
        nume: "Denisa",
        varsta: 24
    }];
utilizatori.sort((first, second) => first.varsta - second.varsta);
//[ { nume: 'Denisa', varsta: 24 }, { nume: 'Andrei', varsta: 25 } ]

Ce am învățat:

  1. Comportamentul implicit al metodei Array.prototype.sort()
  2. Cum implementăm un algoritmul de sortare specific

BONUS: NaN is not a NaN 

Surpriză, asta nu a fost tot!

NaN === NaN //fals
Explicație:

Acest WTF se referă la termenul Strict Equality Comparison și la implementarea lui.

  1. Dacă Type(x) este diferit de Type(y) returnează fals.
  2. Dacă Type(x) este de tip Număr, atunci
    1. Dacă x este NaN, returnează false
    2. Dacă y este NaN, returnează false
  3. Altfel, returnează SameValueNonNumber(x, y).

Cum știm deja, NaN este de tip number, așadar s-a ajuns la al doilea if. După aceasta, dacă oricare dintre operanzi este NaN, se returnează false.

Ce am învățat:

  1. Explicarea primei părți a implementării Strict Equality Comparison
  2. Ultima parte a algoritmului utilizează o funcție numită SameValueNonNumber

Concluzie

Cam astea ar fi cele 6 (7) WTFs despre care vorbeam în titlu. Unele poate par copilărești și da, s-ar putea să ai dreptate în această privință, dar aceste greșeli care par mici la prima vedere pot ascunde bug-uri care ulterior pot avea un impact mare, iar acest lucru va duce cu siguranță la irosirea timpului și a energiei tale. Nu în ultimul rând, obiceiul de a căuta în documentația oficială când ceva pare să fie ciudat în codul tău și apoi să înveți cum gândește compilatorul te ajută să-ți îmbunătățești abilitățile de programator.

Distribuie articolul pe: