Typy danych

W Javascript dane dzielą się na 2 typy: typy proste oraz referencyjne.

typy danych w JS

Typy proste

Dane typu prostego reprezentują proste typy danych - liczby, teksty, wartości boolowskie (prawda/fałsz), niezdefiniowane (undefined) oraz null.


//przykład danych typu prostego
const varNr = 20; //prosta liczba
const text = 'To jest tekst'; //prosty łańcuch znaków
const bol = true; //logiczny - prawda/fałsz
const myNul = null; //nic

const x; //zmienna z nieokreśloną wartością, lub w ogóle nie istniejąca

Powyższe typy danych jak sama nazwa wskazuje są prostymi bytami, które poza wartością nic w sobie nie mają. Żeby wykonywać na nich jakieś operacje (np. pobrać długość tekstu) musiałbyś albo za każdym razem używać na nich osobnych funkcji które konwertują proste byty na coś bardziej złożonego (czytaj obiekty), które dawały by dostęp do odpowiednich metod i właściwości.

W JS nie musimy czegoś takiego robić. Javascript ułatwia nam pracę i gdy chcemy zrobić jakąś rzecz na typie prostym (pobrać długość tekstu, zamienić liczbę na zmienno przecinkową itp.) w tle dokonuje automatycznej tymczasowej konwersji typu prostego na obiekt, odpala użytą właściwość lub metodę, a następnie przywraca daną zmienną do typu prostego:


    const ourText = 'Przykładowy tekst'; //deklarujemy prostą zmienną
    ourText.length //js poza sceną skonwertuje ourText na obiekt String, zwróci jego długość i przywróci zmienną do typu prostego

    const nr = 23;
    nr.toFixed(2); //znowu to samo działanie - numer został na chwilę zamieniony na obiekt, została użyta metoda toFixed po czym przywrócono go do typu prostego

Dzięki powyższej konwersji w javascript wszystko zachowuje się jak obiekt. Zasada ta nie dotyczy się tylko undefined i null, które nie potrzebują mieć właściwości i metod.

Powyższe wymienione typy mają też odpowiedniki w konstruktorach, które od razu robią z nich pełnoprawne obiekty:


const varNr = new Number(20); //liczba
const text = new String('To jest tekst'); //łańcuch znaków
const varBol = new Boolean(true); //logiczny - prawda/fałsz

Czym są konstruktory i z czym to się jenauczysz się później. W powyższych przypadkach nie zaleca się stosować konstruktorów do tworzenia takich obiektów. Chcesz utworzyć tekst, napisz po prostu w cudzysłowach. Chcesz utworzyć liczbę, napisz po prostu liczbę.


const varA = "lorem ipsum";
const varB = new String("lorem ipsum"); //tutaj chciałem zaszpanować i użyłem konstruktora

console.log( varA.length ); //11
console.log( varB.length ); //11

console.log( typeof varA ); //string - prawidłowo
console.log( typeof varB ); //object - niby prawidłowo, ale...
    

Zmienne typu prostego charakteryzują się tym, że ich wartości są przypisane bezpośrednio do zmiennych (są oddzielnymi bytami). Zobaczmy przykład:


let ourVar1 = 12;
let ourVar2 = ourVar1; //do zmiennej ourVar2 przypisujemy wartość 12

ourVar1 = 15; //zmieniam tylko ourVar1

console.log(ourVar1); //15
console.log(ourVar2); //12

Powyższe równanie zachowuje się zupełnie tak, jak nas uczyli na matematyce. Czyli taki totalny klasyk.

Typy złożone

Wszystkie zmienne nie będące typem prostym są obiektami i są typu referencyjnego.
Ten typ danych charakteryzuje się tym, że zmienne nie mają przypisanej bezpośrednio wartości, a tylko wskazują na miejsce w pamięci, gdzie te dane są przetrzymywane.


let arr1 = [1, 2, 3];
let arr2 = arr1; //zmienna arr2 wskazuje na tablicę [1, 2, 3]

arr1.push(4);
console.log(arr1); //1, 2, 3, 4
console.log(arr2); //1, 2, 3, 4

W powyższym przykładzie po zmianie jednej zmiennej, zmieniają się obie. Dzieje się tak właśnie dlatego, że obie zmienne wskazują na ten sam obiekt. Żeby zadziałało to tak jak w typach prostych, druga zmienna musiała by mieć kopię tablicy.


let arr1 = [1, 2, 3];
let arr2 = arr1.slice(); //kopiuje całą tablicę za pomocą metody slice()

arr1.push(4);
console.log(arr1); //1, 2, 3, 4
console.log(arr2); //1, 2, 3

Możliwe, że w tej chwili masz tak zwany "mindfuck". Spokojnie - z czasem wszystkie te rzeczy staną się dla ciebie automatem i nawet nie będziesz się za bardzo nad tym zastanawiał.

Podsumowując:
Typy proste są proste. Mają wartość przypisaną do siebie. To co nie jest typem prostym jest obiektem. Zmienne obiektowe nie mają przypisanej bezpośrednio wartości, a wskazują tylko na miejsce w pamięci. Przy pracy z typami prostymi za naszymi plecami są one konwertowane na bardziej złożone obiekty, a zaraz potem znowu przywracane na typy proste. Uff...

Konwersja typów

JavaScript nie wymaga od ciebie abyś deklarował typ zmiennych. Przykładowo możesz utworzyć zmienną typu liczbowego o nazwie np. someVar, a następnie przypisać jej wartość znakową:


let someVar = 10;
someVar = "to jest napis";

Z jednej strony jest to fajne, bo upraszcza sprawę. Z drugiej może powodować błędy w bardziej skompilowanych skryptach. Dlatego też w każdym większym języku (np C++) konieczne jest określanie typów danych. Dlatego też powstały dla JS takie nakładki jak TypeScript

JS lubi być automatyczny. Tak jest w przypadku operacji na typach prostych i kilku innych momentach (np. w przypadku hoistingu zmiennych czy deklaracji funkcji).
Jeżeli byśmy chcieli dodać do zmiennej typu znakowego zmienną typu liczbowego wówczas otrzymalibyśmy wynik typu znakowego:


const someVar = "to jest napis " + 20;
console.log(someVar); //"to jest napis 20"

const someVar = "20" + 1;
console.log(someVar);  //"201"

const someNumber1 = "20";
const table1 = [1];
console.log(someNumber1 + table1); //"20", bo tablica została skonwertowana na ""

const someNumber2 = "20";
const table2 = [1,2,3];
console.log(someNumber2 + table2); //"201,2,3"

const someNumber3 = "22";
const ob = {};
console.log(someNumber3 + ob); //"20[object Object]"

Dzieje się tak dlatego, że JS automatycznie dokonuje konwersji typów tak, by móc przeprowadzić powyższe działania.

Dodawanie jest łatwym działaniem, dlatego nawet jak dodajemy typy danych, których teoretycznie dodać się nie da, dostajemy nie błąd, a jakiś wynik (który sensowny nie jest, ale przynajmniej nie ma błędu).

Dla dociekliwych: czemu dwa ostatnie równania z powyższego przykładu zwracają właśnie taki, a nie inny wynik? Odpowiedzialna jest za to metoda toString, która brana jest z prototypu wszystkich obiektów, która konwertuje dany obiekt na odpowiedni wpis gdy dana zmienna musi być skonwertowana na tekst.

Odejmowanie już tak pobłażliwe nie jest.
Gdy od zmiennej typu znakowego w której skład wchodzą tylko znaki cyfr odejmiemy zmienną typu liczbowego wówczas wykonamy normalne równanie:


const someVar = "21" - 1;
console.log(someVar); //20

Ale już od zmiennej typu znakowego w której skład wchodzą nie tylko znaki cyfr ale i litery odejmiemy zmienną typu liczbowego wówczas otrzymanym wynikiem będzie NaN (Not-A-Number)


const someVar = "20a" - 1;
console.log(someVar); //NaN

Podobna zasada działa w drugą stronę. Jeśli od zmiennej typu znakowego odejmiemy zmienną typu liczbowego, to w zależności czy pierwsza someVar zawiera litery czy też nie - otrzymamy wynik liczbowy lub typu NaN:


const someVar = 10 - "5";
console.log(someVar);  //5

const someVar2 = 10 - "a3";
console.log(someVar); //NaN

const tab = []
console.log(tab - 20); //-20, bo tablica została skonwertowana na "", co w wyniku daje 0

const ob = {}
console.log(ob - 20); //NaN

Podobne konwersje odbywają się przy mnożeniu i dzieleniu.

Powyższe konwersje mogą wydawać się zawiłe i w praktyce mogą prowadzić do błędów. Dlatego dobrą praktyką jest korzystać z manualnych funkcji konwertujących:


parseInt(str, system_liczbowy); //parsuje tekst na liczbę całkowitą
parseFloat(str); //parsuje tekst na liczbę
Number(str); //konwertuje tekst na liczbę

Powyższe metody różnią się tym, że parseInt i parseFloat próbują skonwertować tekst, który może w sobie zawierać litery.
Funkcja Number nie jest już tak wybredna i str może zawierać tylko liczby:


console.log( Number("100")); //100
console.log( Number("50.5")); //50.5
console.log( Number("50px")); //NaN

console.log( parseInt("24px", 10)); //24
console.log( parseInt("26.5", 10)); //26
console.log( parseInt("100Dog", 10)); //100
console.log( parseInt("Hello", 10)); //NaN

console.log( "20px" + "20px"); //20px20px
console.log( parseInt("20px", 10) + parseInt("20px", 10) + "px"); //40px

Ważne jest, by przy wykorzystaniu metody parseInt podawać system na jaki chcemy konwertować (drugi parametr). Najczęściej będzie to system 10. Jeżeli tego nie zrobimy, czasami wyniki mogą nas zaskoczyć:


console.log( parseInt("0x200")); //512
console.log( parseInt("0x200", 10)); //0

Istnieją też inne, mniej znane metody konwersji tekstu na liczby:


console.log( +"20" ); //20
console.log( "20"*1 ); //20

Sprawdzanie typu danych

Do sprawdzenia typu danych korzystamy z instrukcji typeof:


const num = 10;
const str = 'przykładowy tekst';
const arr = [];
const obj = {};
const nul = null;
//zmiennej und specjalnie nie zadeklarowałem

//wypisujemy typy zmiennych
console.log( typeof(num) ); //"number"
console.log( typeof(str) ); //"string"
console.log( typeof(arr) ); //"object" ???
console.log( typeof(obj) ); //"object"
console.log( typeof(und) ); //"undefined"
console.log( typeof(nul) ); //"object"

//sprawdzamy typy zmiennych
if (typeof num === "number") {...}
if (typeof str === "string") {...}
if (Array.isArray(arr)) {...}
if (typeof obj === "object") {...}
if (typeof und === "undefined") {...}
if (nul === null) {...}

Zwróć uwagę, jaki typ zwróciła tablica. Tak! Tablica w JS też jest obiektem.
Żeby sprawdzić jej typ, albo musielibyśmy sprawdzać jej konstruktor (poznasz to później), albo skorzystamy z zapisu Array.isArray(tab). Więcej na ten temat możesz dowiedzieć się tutaj.
Inną kluczową rzeczą którą powinieneś tutaj zapamiętać to sposób sprawdzania nieistniejących zmiennych. Nie raz będziesz tego używał.

Podobne "dziwy" dzieją się dla typu null, który w js zwraca "object". Jest to błąd javascripti. Na szczęście bardzo to nie przeszkadza, bo wystarczy taką zmienną przyrównać do null. W końcu null to null...

Trening czyni mistrza

Poniżej zamieszczam kilka zadań, które w ramach ćwiczenia możesz wykonać:

  1. Wymień z pamięci wszystkie typy proste i referencyjne występujące w js.
  2. Stwórz zmienne każdego typu. Wypisz w konsoli ich typy. Spróbuj wykonać na nich proste działania dodawania, odejmowania, mnożenia i dzielenia.
    
                let nr = 23;
                let txt = "ala ma kota";
                let undef;
                let nul = null;
                let obj = {};
    
                console.log(nr + txt);
                console.log(nr + undef);
                console.log(nul + obj);
                ...
                
  3. Masz przykładową zmienną:
    let width = "20px";
    Dodaj do niej wartość 30. Wynik wypisz w konsoli.
    
                let width = "20px";
                let sum = parseInt(width, 10) + 30;
                console.log(sum);
                
  4. Sprawdź czy zmienna age istnieje. Jeżeli nie istnieje, stwórz taką zmienną i wypisz ją w konsoli.
    
                if (typeof age === "undefined") {
                    let age = 23;
                    console.log(age);
                }