PodstawyProgramowanieTypy danych
#JS#javascript#typy#data#types#podstawy#number
Opublikowany 30.05.2024
Czas kontynuować naszą przygodę z typami danych w JavaScripcie. Do tej pory omówiliśmy je ogólnie oraz skupialiśmy się na stringach. Czas więc zmierzyć się z kolejnym typem prostym/prymitywnym, czyli number.
Numerologia
Jak może pamiętacie z tego artykułu wartość typu number deklarujemy w następujący sposób:
const myNumber = 10;
Jest to dużo prostsze, niż w przypadku stringów, gdzie do wyboru było kilka sposobów deklaracji. Niemniej należy pamiętać, że samą liczbę możemy już zapisać różnorako. Posłużymy się przykładem wziętym bezpośrednio z MDN:
255; // two-hundred and fifty-five
255.0; // same number
255 === 255.0; // true
255 === 0xff; // true (hexadecimal notation)
255 === 0b11111111; // true (binary notation)
255 === 0.255e3; // true (decimal exponential notation)
Poza samym faktem, że wartości dziesiętne zapisujemy poprzez użycie kropki, to do deklaracji liczby nie musimy ograniczać się do systemu dziesiętnego (czyli tego używanego przez nas na co dzień). Jak zapewne się domyślacie, z deklaracji liczby w systemie innym, niż dziesiętny korzystamy ekstremalnie rzadko i raczej mając ku temu konkretny powód.
Dodając dwa do dwóch - czyli odrobina matematyki
Paul Claudel powiedział, że "liczba jest zaczątkiem i korzeniem przestrzeni". Co bardziej poetycko nastrojeni fizycy mawiają, że liczby są alfabetem opisującym wszechświat. Kłócił się z tym nie będę, bo poeta ze mnie jeszcze marniejszy, niż fizyk. Jednak dla nas - przynajmniej tutaj - liczby będą narzędziem. Narzędziem, za pomocą którego będziemy uzyskiwać oczekiwane (zazwyczaj) efekty.
Pierwszym, co przychodzi na myśl, kiedy słyszymy liczba jest chyba matematyka. JavaScript udostępnia nam całą gamę podstawowych działań matematycznych - czy, jak powinno się je poprawnie nazywać, arytmetycznych:
- dodawanie,
- odejmowanie,
- mnożenie,
- dzielenie,
- modulo (reszta z dzielenia)
- potęgowanie.
Ktoś dociekliwy może zawołać "a co z pierwiastkowaniem?!". No cóż... przecież jest 😉 jeśli cofniemy się choć na chwilkę (o dłużej nie śmiem prosić) do czasów szkolnych, wtedy być może przypomni się nam, że pierwiastek stopnia n to nic innego, jak potęga 1/n 😊
Ok, wiemy już, co możemy wykonać i mamy za sobą krótki powrót do szkolnej traumy. To teraz podstawowe pytanie: jak te działania zapisać? W zasadzie... w większości przypadków bardzo intuicyjnie. Zapraszam do poniższego przykładu:
const addition = 2 + 3; //dodawanie
const substract = 2 - 3; //odejmowanie
const multiplication = 2 * 3; //mnożenie
const division = 2 / 3; //dzielenie
const modulo = 2 % 3; //reszta z dzielenia - w tym przypadku reszta z dzielenia 2/3
const exponent = 2 ** 3; //potęgowanie - w tym przypadku 2^3
Działania oczywiście można łączyć, pamiętając o kolejności ich wykonywania. Niestety przy bardziej zaawansowanych działaniach trzeba trochę pokombinować.
Jak mus, to mus!
W artykule o stringach była mowa o konwersji typu. Tam dotyczyło to konwersji niejawnej, ale trzeba pamiętać, że mamy też do dyspozycji konwersję wymuszoną. W praktyce oznacza to ni mniej, ni więcej, ale że możemy zmusić dane, żeby przyjęły inny typ, niż ich początkowy. Fajne? No... jeśli robimy to z głową, to tak 😉
Jak może pamiętacie, to w przypadku próby dodania do siebie zmiennych typu string i number interpreter domyślnie przekonwertuje number do stringa. Czyli dostaniemy takie coś:
const number = 2;
const string = '11';
const add = number + string; //'112'
typeof add; //'string'
Wymuszenie konwersji pozwala się zabezpieczyć przed takimi sytuacjami - możemy ręcznie przekonwertować string do number i dopiero je do siebie dodać.
const a = '11';
const b = 2;
const sum = Number(a) + Number(b); //13
typeof sum; //'number'
Czy konstruktor Number() przyjmuje tylko string? Oczywiście, że nie. Można go uży ć z dowolnym typem, choć efekty nie zawsze będą oczywiste.
Number(5); //5
Number(true); //1
Number(false); //0
Number(undefined); //NaN
Number(null); //0
Number(1234567890123456789012345678901234567890n); //1.2345678901234568e+39
Number(Symbol()); //TypeError
Inna metodą na konwersję do typu number jest postawienie znaku + przed zmienną. Działa niemal identycznie, jak konstruktor Number - z jednym wyjątkiem. Konwersja wartości typu BigInt zakończy się otrzymaniem TypeError.
const big = 1234567890123456789012345678901234567890n;
const number = '12344';
Number(big); //1.2345678901234568e+39
+number //12344
+big //TypeError
W sytuacji, kiedy JavaScript stara się przekonwertować coś do typu number w sposób niejawny - wówczas działa to tak, jak w sytuacji z +.
Teraz mała zagadka. Spójrz na poniższego mema i zastanów się, dlaczego jest on prawdziwy 😋
Dodatkowo można się zastanowić, jak dodając tylko jeden znak do pierwszego równiania otrzymać wynik 12 😊
W kwestii konwersji stringów istnieją jeszcze dwa sposoby, analogiczne w działaniu do Number(). Są to metody parseInt() oraz parseFloat().
Number.parseInt()
Metoda parseInt() (lub Number.parseInt() - oba zapisy działają tak samo) pozwala na konwersję do liczby całkowitej (ang. Integer). Do dokładnych różnic w użyciu parseInt() oraz Number() jeszcze przejdziemy, ale kilka spraw należy omówić już w tym miejscu.
Po pierwsze - parseInt() (oraz parseFloat(), ale o nim za moment) jako argument może przyjąć jedynie dane typu string, number oran BigInt. W każdym innym przypadku wynikiem będzie NaN.
Kolejną istotną sprawą, o jakiej w przypadku parseInt() należy powiedzieć, to sposób, w jaki jest zwracana liczba całkowita. Trzeba bezwzględnie pamiętać, że parseInt() nie zaokrągla liczby zgodnie z zasadami matematyki, czyli w dół dla wartości mniejszych od 0.5 i w górę dla pozostałych. Metoda parseInt() zawsze zaokrągla w dół!
const decimalNumber = 9.13;
const decimalNumberCloseToBigger = 9.98;
const string = '8.34';
const big = 1234567890123456789012345678901234567890n;
parseInt(decimalNumber); //9
parseInt(decimalNumberCloseToBigger); //9
parseInt(string); //8
parseInt(big); //1.2345678901234568e+39
parseInt(false); //NaN
parseInt(true); //NaN
parseInt(undefined); //NaN
parseInt(null); //NaN
Metoda parseInt() może przyjąć string zawierający znaki niebędące liczbami, pod warunkiem, że taki ciąg zaczyna się liczbą. Wówczas metoda zwraca wartość dla liczby ignorując wszystko, co jest dalej. Warto zwrócić uwagę na fakt, że jeśli w ciągu mamy dwie liczby rozdzielone czymś liczbą niebędącym, to pod uwagę będzie wzięta tylko ta pierwsza liczba.
const string = '123asade';
const stringWithDecimal = '12.3fseef';
const stringWithTwoNumbers = '123sdfs321';
const stringWithNumberAndWhiteSpace = '123 adsfas das 3';
const stringWithLetterFirst = 'a12';
parseInt(string); //123
parseInt(stringWithDecimal); //12
parseInt(stringWithTwoNumbers); //123
parseInt(stringWithNumberAndWhiteSpace); //123
parseInt(stringWithLetterFirst); //NaN
Warto też w tym miejscu zaznaczyć, że tzw. białe znaki - czyli np. spacje lub taby - poprzedzające liczbę są ignorowane. Jeśli zaś są one w środku ciągu, to wszystko za nimi jest ignorowane.
parseInt(' 123'); //123
parseInt(' 123 '); //123
parseInt('123 '); //123
parseInt('12 3'); //12
Kolejną cechą parseInt() jest możliwość przyjęcia drugiego argumentu. Argument ten musi być liczbą z przedziału od 2 do 36. Jest to oznaczenie systemu, z jakiego jest konwertowana liczba. Metoda parseInt() może też rozpoznać konkretne systemy:
- ciąg zaczynający się od znaków 0x jest traktowany jak liczba w systemie szesnastkowym,
- liczby zaczynające się od 0 jest traktowana jak liczba w systemie ósemkowym,
- w każdym innym przypadku jak liczba w systemie dziesiętnym.
Wynikiem będzie liczba w systemie dziesiętnym.
parseInt('123'); //123 -> równoważne z parseIint('123', 10)
parseInt('1D78'); //1 -> równoważne z parseIint('1D78', 10)
parseInt('1D78', 16); //7544
parseInt('274', 8); //188
parseInt(057);//47 -> równoważne z parseInt('57', 8)
parseInt('057');//57
parseInt('0110', 2); //6
Number.parseFloat()
Przypadek parseFloat() jest bardzo podobny do wspomnianego przed chwilą parseInt(). Podobnie przyjmuje jako argument wartości typu number, BigInt oraz string. Co zatem odróżnia te dwie metody?
Number.parseFloat() nie zaokrągla wynikowej liczby, jak robi to parseInt(). Jednak także może parsować ciągi składające się z liczby i innych znaków (przy tym samym ograniczeniu, że ciąg musi zaczynać sie liczbą).
const string = '123asade';
const stringWithDecimal = '12.3fseef';
const stringWithTwoNumbers = '123sdfs321';
const stringWithNumberAndWhiteSpace = '123 adsfas das 3';
const stringWithLetterFirst = 'a12';
parseFloat(string); //123
parseFloat(stringWithDecimal); //12.3
parseFloat(stringWithTwoNumbers); //123
parseFloat(stringWithNumberAndWhiteSpace); //123
parseFloat(stringWithLetterFirst); //NaN
W przeciwieństwie do parseInt() metoda parseFloat() przyjmuje tylko jeden argument, co oznacza, że można wprowadzać tylko liczby w systemie dziesiętnym.
To jak z tymi stringami?
Prawdopodobnie zauważyliście, że omawiając konstruktor Number() praktycznie pominąłem stringi. Owszem, wspomniałem o nich, ale pojawiły się tylko w formie będącej ciągiem odpowiadającym formatowi typu number. A przecież typ string może przyjmować dowolny ciąg znaków. Co zatem w sytuacji, jak będziemy starali sie przekonwertować Pana Tadeusza do typu number?
W ogromnym uproszczeniu - jeśli konwertowany string nie jest liczbą, to otrzymamy wartość NaN (pamiętając, że dysponujemy wspomnianymi metodami parseFloat() oraz parseInt()!).
Number('123'); //123
Number('12r4'); //NaN
parseInt('12r4'); //12
A jak ma się sprawa z białymi znakami? Te na początku i na końcu ciągu są ignorowane. Jeśli zaś jakiś pojawi się w środku - wówczas otrzymamy NaN.
Number(' 123'); //123
Number('123 '); //123
Number(' 123 '); //123
Number('12 3'); //NaN
Ciąg może zaczynać sie od znaków - lub +, pod warunkiem, że pojawiają się w ciągu wyłącznie raz i między nimi a liczbą nie ma białych znaków.
Number('+9'); //9
Number('-9'); //-9
Number(' -9 '); //-9
Number('- 9'); //NaN
Ciągi Infinity oraz -Infinity są rozpoznawane jako oznaczenie +/- nieskończoności.
Number('+Infinity'); //Infinity
Number('-Infinity'); //-Infinity
Number('infinity'); //NaN
Puste ciągi oraz ciągi zawierające tylko białe znaki zwracają wartość 0.
Number(''); //0
Number(' '); //-0
Jedynym separatorem, jakiego można użyć jest kropka, odpowiadająca notacji ułamka dziesiętnego.
Number('12.2'); //12.2
Number('12,2'); //NaN
Nanananana, Batm... NaN!
Określenie, że coś zwraca wartość NaN padło w powyższych akapitach bardzo często. Dobra, ale czym ten cały NaN jest?
NaN, od angielskiego Not-A-Number, jest globalną wartością, która mówi, że coś nie jest liczbą.
Fajnie, ale przecież string też może nie być liczbą, a przy okazji omawiania tego typu danych nie wspominałem o NaN. Owszem, ale NaN nie jest właściwością zmiennej, a konkretną wartością. I to wszystko jest dużo prostsze, niż brzmi w pierwszej chwili 😉 Najprościej ujmując NaN można potraktować podobnie, jak np. liczbę PI. NaN to... po prostu NaN 😊
Wartość tę możemy otrzymać w pięciu sytuacjach:
- konwersji czegoś, co nie jest liczbą do typu number,
- działania matematycznego, które nie posiada rozwiązania w liczbach rzeczywistych (np. pierwiastek kwadratowy z liczby ujemnej),
- nieokreślonej formy (np. 0/Infinity),
- metody lub wyrażenia operującego na NaN lub gdzie NaN zostało wymuszone (np. 7 ** NaN, 7 * 'lorem ipsum'),
- w innych przypadkach, gdzie nieprawidłowa wartość ma być reprezentowana jako liczba (np. zły format daty - Date('lorem ipsum).getTime())
Oczywiście nic nie stoi też na przeszkodzie, żeby wartość NaN przypisać do zmiennej ręcznie 😉
const notNumber = NaN;
Wspomniałem wyżej, że NaN otrzymujemy między innymi poprzez wyrażenia, które już na NaN operują. I to jest jak najbardziej prawda. Poza jednym wyjątkiem. Jeśli NaN podniesiemy do potęgi 0, wówczas otrzymamy... 1 😊
NaN ** 0; //1
Cyryl i metody
Podczas omawiania typu string padło stwierdzenie, że typy są obiektami. Jak można się zatem spodziewać obiekt Number, podobnie jak String, będzie miał swoje właściwości i metody. Omówmy zatem te najczęściej używane (z pominięciem już omówionych parseInt() oraz parseFloat()).
Number.isNaN()
Metoda sprawdzająca, czy podana wartość jest NaN. Jako argument przyjmuje dowolną wartość, zaś jej wynikiem jest true lub false.
const string = 'lorem ipsum';
const numberString = '123';
Number.isNaN(Number(string)); //true
isNaN(Number(numberString)); //false
Jak widzicie, metodę można zapisać na dwa sposoby: Number.isNaN lub samo isNaN.
Number.isInteger()
Metoda sprawdzająca, czy podana wartość jest liczbą całkowitą. Jako argument przyjmuje dowolną wartość, zwraca true lub false.
const string = 'lorem ipsum';
const numberDecimals = 123.5;
const number = 123;
Number.isInteger(string); //false
Number.isInteger(numberDecimals); //false
Number.isInteger(number); //true
Number.prototype.toFixed()
Metoda pozwalająca zaokrąglić liczbę z określoną dokładnością. Jako argument może przyjąć liczbę będącą ilością miejsc po przecinku, do której nastąpi zaokrąglenie. Jeśli nie podamy argumentu, wówczas liczba zostanie zaokrąglona do liczby całkowitej.
const number = 123.456;
const anotherNumber = 123.454;
number.toFixed(2); //123.46
anotherNumber.toFixed(2); //123.45
number.toFixed(); //123
Number.prototype.toString()
metoda toString(), jak sama nazwa sugeruje, konwertuje liczbę do stringa. Może przyjąć argument będący liczbą całkowitą z zakresu od 2 do 36. Argument ten wskazuje, w jakim systemie będzie zwrócona liczba, zanim zostanie przekonwertowana do stringa. Domyślnie (w przypadku braku argumentu) jest to 10 - czyli system dziesiętny. Jeśli argument nie będzie spełniał wymogu (będzie mniejszy od 2, większy niż 36 lub nie będzie liczbą) wówczas otrzymamy RangeError.
const number = 1234;
number.toString(); //'1234'
number.toString(2); //'10011010010'
number.toString(16); //'4d2
number.toString(1); //RangeError
number.toString('kangur'); //RangeError
Po więcej odsyłam na MDN.
Serio myśleliście, że już nie będzie matematyki?
W akapicie o działaniach arytmetycznych na typie number wspomniałem, że "przy bardziej zaawansowanych działaniach trzeba trochę pokombinować". Jeśli czytając to zaczęliście się zastanawiać, jak ogarnąć funkcje trygonometryczne czy inne logarytmy, to spokojnie. Nie byliście pierwsi. Ktoś się już nad tym głowił - i, co więcej, dał nam narzędzie, żeby poradzić sobie z nieco bardziej zaawansowanymi działaniami.
Obiekt Math
JavaScript oferuje nam globalny obiekt Math, posiadający metody udostępniające nam zaawansowane funkcje oraz stałe matematyczne. Omówmy te najpopularniejsze.
Math.random()
Z tą metodą z pewnością, prędzej czy później, się spotkacie. Match.random() nie przyjmuje argumentów i zwraca liczbę pseudolosową z zakresu od 0 do 1. Oczywiście można, stosując pewną sztuczkę, uzyskać dowolną liczbę - ale o tym za moment.
Math.random(); //0.8258880753334936
Math.round()
Metoda Math.round() przyjmuje liczbę jako argument i zwraca jej zaokrąglenie do najbliższej liczby całkowitej.
Math.round(5.234); //5
W połączeniu z Math.random() pozwala generować liczby pseudolosowe z dowolnego zakresu. Skorzystamy do tego z następującego wzoru: Math.round(Math.random() * (max + 1)), gdzie max to największa liczba, jaką możemy wygenerować.
Jeśli chcemy generować liczbę, która nie będzie mniejsza, niż zadana wartość - także mamy taką możliwość. Wówczas nasz wzór będzie wymagał drobnej modyfikacji: Math.round(Math.random() * (max + 1 - min) + min), gdzie max to największa a min najmniejsza liczba, jaką możemy wygenerować.
Math.round(Math.random() * 20); //9
Math.round(Math.random() * 15 + 5) //15
Pierwszy z powyższych przykładów pozwala wygenerować liczbę pseudolosową z zakresu 0 - 19. W drugim przypadku otrzymamy liczbę z zakresu 5 - 19.
Math.ceil(), Math.floor()
Metody Math.ceil() i Math.floor() działają bardzo podobnie do Math.round(). Różnica polega na sposobie zaokrąglania. Math.ceil() zaokrągla zawsze do wyższej lub równej liczby całkowitej, natomiast Math.floor() do niższej lub równej.
Math.ceil(5.234); //6
Math.ceil(6); //6
Math.floor(5.999); //5
Math.floor(6); //6
Stałe matematyczne
Math.E; //liczba Eulera
Math.PI // liczba PI
Math.SQRT2 // pierwiastek kwadratowy z 2
Trygonometria
Math.sin(x); //sinus z x
Math.cos(x); //cosinus z x
Math.tan(x); //tangens z x
Math.asin(x); //arcus sinus z x
Math.acos(x); //arcus cosinus z x
Math.atan(x); //arcus tangens z x
Math.sinh(x); //sinus hiperboliczny z x
Math.cosh(x); //cosinus hiperboliczny z x
Math.tanh(x); //tangens hiperboliczny z x
Logarytmy
Math.LN10; //logarytm naturalny z 10
Math.LN2; //logarytm naturalny z 2
Math.LOG10E; //logarytm o podstawie 10 z e
Math.LOG2E; //logarytm o podstawie 2 z e
Math.log(x); //logarytm naturalny z x
Math.log10(x); //logarytm dziesiętny z x
Math.log2(x); //logarytm o podstawie 2 z x
Inne przydatne
Math.sqrt(x); //pierwiastek kwadratowy z x
Math.exp(x); //e^x
Math.min(x,y,z); //zwraca najmniejszą liczbę z podanych jako argumenty
Math.max(x,y,z); //zwraca największą liczbę z podanych jako argumenty
Math.exp(x); //e^x
Math.abs(x); //zwraca wartość bezwzględną z x
Jak zawsze zachęcam do sprawdzenia wszystkich metod i właściwości obiektu Math.
Dość już tych cyferek!
Tak, wiem. Było sporo wzorów i liczb. Ale spokojnie - jeśli samo słowo matematyka przyprawia was o dreszcze, to na frontendzie nie ma tej matmy za wiele. W codziennej pracy będziecie używać podstawowych działań arytmetycznych i kilku metod obiektu Math.
Najprawdopodobniej następnym razem pomówimy sobie o typie boolean. Tymczasem, tradycyjnie już, zapraszam na mojego twixera 😉
Do następnego!
Myśleliście, że zapomniałem? Rozwiązanie zagadki.
Mem pokazuje nam dwa równania, w których przeprowadzana jest operacja arytmetyczna z użyciem danych typu string i number:
'11' + 1;
'11' - 1;
Zastanówmy się zatem, co się tu dzieje.
Po pierwsze JavaScript musi doprowadzić obie wartości do wspólnego typu. Jak może pamiętacie - przy dodawaniu stringa i numeru JS domyślnie drąży do konkatenacji stringów, zatem przekonwertuje liczbę 1 do stringu '1'. Następnie połączy oba ciągi i w efekcie otrzymamy ciąg '111'.
W drugim przypadku, ponieważ odejmowanie jest działaniem typowo liczbowym, JavaScript będzie się starał przekonwertować string do typu number - dlatego na miejsce ciągu '11' otrzymamy liczbę 11, od której już bezproblemowo można odjąć 1 😊
'11' + 1; //'111'
'11' - 1; //10
Jeśli zaś chcielibyśmy w pierwszym równaniu otrzymać wynik liczbowy za pomocą dodania tylko jednego znaku, to wystarczy przekonwertować string za pomocą znaku (+).
+'11' + 1; //12
Proste, prawda? 😉