Typy zmiennych w js

Typy danych w javascript dzielą się na typy prymitywne (proste) oraz złożone. Przyjrzyjmy się im bliżej.

Typy proste

Typy proste lub prymitywne, nazywane również czasem wartościowymi (zaraz sobie powiemy dlaczego) są najprostszym typem danych w js. Typ zmiennej podczas deklaracji jest przypisywany do niej dynamicznie - nie musimy podawać konkretnego typu zmiennej przed jej deklaracją jak to bywa w innych językach, silnie typowanych. Przykładowo, przypisując ciąg znakowy do nowo zadeklarowanej zmiennej domyślnie otrzymamy typ string. Można to sprawdzić za pomocą operatora typeof. W javascript rozróżniamy następujące typy zmiennych prostych/prymitywnych:

const number = 7;
const someBigNumber = BigInt(9007199254740991);
const text = 'some string';
const toBe = true;
const existing = undefined;
const symbol = Symbol(1);

Są to kolejno: Number, BigInt, String, Boolean, Undefined, Symbol. Czyli kolejno typ liczbowy, wielkoliczbowy, tekstowy, logiczny, niezindentyfikowany oraz dodany w ES6 typ symbol. Warto dodać, że sam javascript ma nieco więcej typów prostych, ale operują one głównie na tych powyższych, np. aby otrzymać zmienną typu float (zmiennoprzecinkową), musimy przypisać jej wartość właśnie z częścią po przecinku. Nie chcę się dziś jednak skupiać na specyfikacji i prototypie każdego typu, bo to wiedza dość obszerna i temat raczej na osobne wpisy. Chciałbym natomiast wyjaśnić ideę typów w javascript ogólnie.

Typy złożone

Typy złożone (a w zasadzie jest tylko jeden taki typ) to obiekty. Mówi się, że javascript to język obiektowy. To w pewnym rozumieniu prawda, ale nie ma to raczej związku z samymi obiektami w js jako typy danych, tylko z prototypami - każda zmienna/typ danych ma swój prototyp, który oferuje tworzenie nowych obiektów. Również dla typów, takich jak number czy string. Ale to także temat na inny czas. W javascripcie natomiast obiekty jako typy to tak naprawdę typy referencyjne, jak: Object, Array, Function, Map, WeakMap, Set, WeakSet, Date. czyli tak naprawdę wszystko to, co można utworzyć za pomocą operatora new.

To oznacza, że każda z tych zmiennych po użyciu operatora typeof będzie zwracała wartość object. Wyjątkiem jest tutaj prototyp funkcji, który dziedziczy prototyp obiektu, ale jego zwracana wartość to function - twórcy specyfikacji ECMAscript zapewnili tutaj shortand, który ułatwia pracę developerów. Zanim przejdziemy do najistotniejszej różnicy pomiędzy typami prostymi i złożonymi, nadmienię jeszcze, że istnieje typ zmiennej null, który jest typem jakoby specjalnym - nadal prymitywnym, lecz zwraca on wartość obiektu, ponieważ każda inna obiekt jest pochodny od null.

Najważniejsza różnica pomiędzy typami

Najistotniejszą różnicą między typami prymitywnymi a złożonymi jest sposób przekazywania ich instancji. Wspomniałem wcześniej, że typy proste nazywane są również wartościowymi. Oznacza to, że wartości przekazywane przez zmienne prostego typu są przekazywane przez wartość. Co to w praktyce oznacza? Rozważmy następujący przypadek:

const a = 5;
let b = a;
b = b + 3;

Mamy tutaj dość klasyczny przykład deklaracji nowych zmiennych. Do zmiennej a została przypisana wartość prymitywna 5 - uruchomił się wtedy domyślny konstruktor Number dla wartości liczbowych. Następnie zadeklarowana została zmienna b, której przypisana została wartość zmiennej a. Potem dodane zostało 3 do zmiennej b, więc w rezultacie otrzymana wartość to 8. Zmienna a została przy tej operacji nie naruszona i jej wartość nadal wynosi pierwotne 5. Spróbujmy zrobić podobną operację na tablicy:

const a = [1, 1, 3, 7];
let b = a;
b.push(3);

Można by było się spodziewać, że zadziała to, tak jak przy wartościach prymitywnych, jednak typy złożone charakteryzują się tym, że są przekazywanę przez referencję. Oznacza to, że wykonując operację na zmiennej b zmodyfikujemy również instancję przypisaną do zmiennej a. Jak sobie z tym radzić? To zależy od prototypu danego typu referencyjnego. Przykładowo na skopiowanie struktury obiektu jest 7 różnych sposobów, a jednym z nich jest ożycie funcji prototypu funkcji, a mianowicie Object.assign:

// obiekt przekazany przez referencję

const a = {
    firstName: 'Damian',
    lastName: 'Kalka'
}

const b = a;
b.age = 22;

// obiekt skopiowany do nowej zmiennej

const c = Object.assign({}, a);
c.age = 22;

Do skopiowania wartości tablicy można użyć przykładowo spread opearatora (...):

const a = [1, 1, 3, 7];
let b = [...a];
b.push(3);    // wykonanie funkcji push nie zmodyfikuje pierwotnej instancji

Wyjątkiem w typach złożonych jest typ funkcji, która nie zostaje przekazana przez referencję, a przez wartość. Aby otrzymać referencję funkcji musimy posłużyć się na przykład mechanizmem domknięcia.

Typy danych jako istotna rzecz na początku

Znajomość typów oraz sposobu ich działania stanowią fundamentalną wiedzę javascript developera i nie należy jej lekceważyć. Wprawne posługiwanie się typami zapewni szybkie zrozumienie działania języka i zapobieże trudności w radzeniu sobie z nieoczekiwanymi błędami podczas przyszłej nauki. To samo tyczy się frameworków i bibliotek - wszystkie one są operte na czystym (vanilla) JS. Jasne jest więc, że znajomość mechanizmów fundamentalnych to konieczność zanim wejdziemy na głęboką wodę.

Źródła

© Damian Kalka 2021
Wszelkie prawa zastrzeżone