PodstawyProgramowanieTypy danych
#JS#javascript#typy#data#types#podstawy#null#undefined
Posted on 20.08.2024
Ehhh... wakacje... Najpierw starasz się nadgonić jak najwięcej, żeby mieć chwilę na odpoczynek. A potem trzeba jeszcze odpocząć po odpoczynku 😉 Także nie, nie zapomniałem o blogu i będę was męczył JSem jeszcze przez jakiś czas 😋
Nasza przygoda z typami prymitywnymi dobiega końca. Wiem, że nie wszystko dokładnie omówiliśmy - ale typy BigInt i Symbol są używane na tyle rzadko, że ich dokładniejsze opisanie byłoby stratą waszego czasu. Przynajmniej na poziomie podstawowym.
Do omówienia zostały dwa typy, które są bardzo istotne i jednocześnie wydają się bardzo podobne. Czy tak jest w rzeczywistości? Przyjrzyjmy się typom null oraz undefined!
Czy czegoś może nie być bardziej?
Wiem, że ten mem już się pojawił we wprowadzeniu do typów w JS. Ale jest on na tyle dobry, że pozwolę sobie go przytoczyć jeszcze raz:
Ostatni przykład, czyli NaN sobie odpuścimy - ta wartość była dosyć dokładnie opisana w artykule o typie Number.
Czym w takim razie różnią się wartości 0, null i undefined?
Wartość 0 to nic innego jak liczba. Przykładowo, jeśli miałem w portfelu 10 zł i je wydałem - wówczas pozostaje mi 0 zł. Nic niezwykłego.
Nieco inaczej sprawa wygląda w przypadku null - czyli jedynej wartości, jaką mogą przyjmować dane typu null właśnie.
Null informuje nas, że dana zmienna istnieje, ale intencjonalnie nie posiada wartości. I nie jest to jednoznaczne z zadeklarowaniem zmiennej bez wartości!
Wracając do przykładu z portfelem - mielibyśmy null pieniędzy w chwili, gdybyśmy celowo nie wzięli portfela.
W tym miejscu jeszcze jedna ważna uwaga. Ze względu na kompatybilność wsteczną JavaScriptu, jeśli sprawdzimy typ wartości null za pomocą metody typeof, to otrzymamy object.
typeof null; //'object'
Jak wygląda sprawa z undefined? Zasadniczo jest to wartość podobna do null. Podobna - ale nie taka sama. Undefined informuje nas, że zmienna nie istnieje lub została zadeklarowana bez przypisania jej wartości.
W przykładzie z gotówką - jeśli zapomnieliśmy portfela, lub przenieślibyśmy się w miejsce, gdzie waluta nie istnieje, to wówczas nasze finanse będą undefined.
W ramach podsumowania - krótkie porównanie wartości null i undefined:
typeof null; // 'object'
typeof undefined; // 'undefined'
null === undefined; // false
null == undefined; // true
null === null; // true
null == null; // true
!null; // true
Number.isNaN(1 + null); // false
Number.isNaN(1 + undefined); // true
Undefined czy ReferenceError?
Chyba każdy, kto już cokolwiek zaczął grzebać w kodzie, spotkał się z błędem ReferenceError.
Uncaught ReferenceError: myVariable is not defined
Ten błąd mówi, że zmienna myVariable nie została zdefiniowana. Czy nie powinna w takim razie zwracać wartości undefined?
Niby tak... ale nie.
Logika mówi, że wartość undefined odpowiada właśnie danym, które nie zostały zdefiniowane. I tak też jest - ale wyłącznie w przypadku elementów w obiektach! W przypadku, kiedy będziemy próbowali się odwołać do zmiennej, której nazwa nigdy się nie pojawiła - wówczas JavaScript ciśnie nam na twarz ReferenceError.
Wspomniałem także o definiowaniu zmiennej bez nadawania jej wartości - i że nie zwróci ona wartości null.
Deklaracja zmiennej bez przypisywania jej wartości nie jest, wbrew pozorom, niczym niezwykłym. Nie jest to dziś często stosowana praktyka - ale jak ktoś bardzo chce, to może tak zrobić.
Jeśli chcemy przypisać wartość do zmiennej nieco później - na przykład po zaciągnięciu jej z API lub pobraniu jej od użytkownika - i jednocześnie mieć pewność, że jej wcześniejsze wywołanie nie skończy się otrzymaniem ReferenceError, to możemy wspomóc się zadeklarowaniem zmiennej bez wartości. Czy jest to eleganckie rozwiązanie? Zdecydowanie nie. Czy zadziała? Jak najbardziej. Trzeba tylko pamiętać o jednej, bardzo istotnej sprawie.
Słowo kluczowe const nie pozwala na deklarowanie zmiennych bez wartości. Ma to o tyle sensu, że zmiennych zadeklarowanych za jego pomocą nie można nadpisywać. Dlatego jeśli zdecydujemy się na takie rozwiązanie, to musimy użyć deklaracji z wykorzystaniem słów kluczowych let lub var:
const myConst;
let myLet;
var myVar;
myConst; //Uncaught ReferenceError: myConst is not defined
myLet; //undefined
myVar; //undefined
Wspomniałem o niezdefiniowanych elementach w obiekcie (o obiektach opowiemy sobie innym razem). Jeśli będziemy chcieli odwołać się do nieistniejącego elementu w istniejącym obiekcie, wówczas otrzymamy wartość undefined.
const obj = {
a: 1,
b: 2,
c: 3
};
obj.a; //1
obj.b; //2
obj.c; //3
obj.d; //undefined
const arr = ['a','b','c'];
arr[0]; //'a'
arr[1]; //'b'
arr[2]; //'c'
arr[3]; //undefined
Consty, lety i vary a sprawa pols... a undefined
Jak wiadomo, zmienne w JS deklarujemy przy użyciu jednego z trzech słów kluczowych: const, let lub var. Prawdopodobnie kiedyś omówimy je sobie dokładniej. W tym momencie trzeba tylko zapamiętać, że notacja const i let jest nowsza i ogólnie zalecana. Miejsce var powinno być już tylko w muzeum i kodzie legacy.
Nie zmienia to jednak faktu, że nadal na zapis z użyciem var można trafić w czasie pracy.
Przejście na zapis z użyciem nowych słów kluczowych nie tylko pozwala na odróżnienie wartości stałych od tych, które mogą być nadpisane. Zmienne deklarowane z użyciem var są inicjowane w nieco inny sposób - co może przysporzyć kilku problemów.
"w mej piwnicy wybieram swój upadek, który jest wyniesieniem"
Zanim przejdziemy do problemu zawiązanego z const, let, var i undefined musimy powiedzieć kilka słów o tzw. hoistingu (z ang. wynoszenie lub podnoszenie).
Jest to mechanizm, który powoduje, że deklaracje zmiennych i funkcji są przenoszone na początek ich zakresu podczas fazy kompilacji. W praktyce oznacza to, że zmiennych i funkcji można używać w kodzie przed ich faktycznym zadeklarowaniem.
Fajnie. Tylko co z tego? Otóż to, że decyzja, jakiego słowa kluczowego użyjemy do deklaracji zmiennej, będzie wpływać na to, jak ta zmienna będzie hoistowana.
W przypadku zmiennych zadeklarowanych za pomocą var, ich deklaracja jest hoistowana na początek zakresu, ale przypisanie wartości pozostaje na swoim miejscu. Dlatego, jeśli próbujesz użyć zmiennej przed przypisaniem, jej wartość będzie undefined.
Zmiennych zadeklarowanych za pomocą let i const również dotyczy hoisting, ale z tą różnicą, że nie są one inicjalizowane podczas hoistingu. Próbując użyć ich przed przypisaniem wartości, napotkamy błąd ReferenceError, ponieważ znajdują się one w tzw. "temporal dead zone" (po naszemu - tymczasowej martwej strefie).
console.log(a); // undefined
var a = 10;
console.log(a); // 10
console.log(b); // ReferenceError: Cannot access 'b' before initialization
const b = 20;
console.log(b); // 20
Do hoistingu i zakresu jeszcze wrócimy w osobnym wpisie, bo to niezwykle ważne zagadnienia.
Błędy, ach, te błędy...
Undefined w JS może przysporzyć nieco kłopotów, szczególnie jeśli nie jest odpowiednio zarządzany. Dlatego warto omówić kilka potencjalnych problemów związanych z undefined oraz wskazać, jak uniknąć potencjalnych błędów.
Praca ze zmiennymi niezainicjalizowanymi
Gdy zmienna jest zadeklarowana, ale nie przypisano jej wartości, automatycznie przyjmuje ona wartość undefined. Praca z takimi zmiennymi może prowadzić do nieoczekiwanych rezultatów, zwłaszcza w operacjach matematycznych lub logicznych.
let x;
console.log(x + 5); // NaN (Not a Number)
W przykładzie zmienna x ma wartość undefined, a operacja daje wynik NaN, co może powodować dalsze problemy w kodzie.
Uniknięcie tego jest banalnie proste. Zawsze inicjalizuj zmienne w momencie ich deklaracji lub sprawdzaj, czy zmienna ma przypisaną wartość przed jej użyciem.
const x = 0;
console.log(x + 5); // 5
Niekontrolowany dostęp do niezdefiniowanych właściwości obiektu
Jak już mówiliśmy, odwoływanie się do właściwości obiektu, która nie istnieje, zwróci undefined. Może to prowadzić do błędów, jeśli spróbujemy operować na wyniku, który oczekuje zdefiniowanej wartości.
const person = { name: "John" };
console.log(person.age); // undefined
console.log(person.age + 5); // NaN
W tym przypadku zmienna person.age nie istnieje, co prowadzi do wyniku undefined, a dalsze operacje na niej mogą prowadzić do błędów.
Dlatego przed dostępem do właściwości obiektu zawsze powinno się sprawdzić, czy istnieje ona w obiekcie, lub użyć wartości domyślnych.
console.log(person.age ? person.age + 5 : "Nie podano wieku"); // "Nie podano wieku"
Błędne Sprawdzanie Warunków z Użyciem undefined
Często undefined jest używane w instrukcjach warunkowych. Jednak jeśli nie jest odpowiednio sprawdzone, może prowadzić do nieoczekiwanych wyników, szczególnie jeśli porównania są wykonywane luźno (z użyciem == zamiast ===).
let value;
if (value == null) {
console.log("Wartość jest null lub undefined");
}
W powyższym przykładzie użycie == spowoduje, że warunek zostanie spełniony zarówno dla null, jak i dla undefined. Może to być mylące, jeśli chcemy rozróżnić te dwie wartości.
Warto wyrobić sobie nawyk używania ścisłego porównania ===, aby uniknąć nieoczekiwanych wyników, oraz jawnie sprawdzać, czy zmienna jest undefined.
if (value === undefined) {
console.log("Wartość jest undefined");
}
Przypadkowe przypisanie undefined
Przypisanie do zmiennej undefined zamiast oczekiwanej wartości może wprowadzać błędy trudne do wykrycia, szczególnie w złożonych aplikacjach.
const result = someFunction();
if (result === undefined) {
console.log("Funkcja nie zwróciła wyniku");
}
Jeśli someFunction powinna zwracać wynik, ale w niektórych przypadkach zwraca undefined, może to prowadzić do nieoczekiwanych zachowań.
Tutaj musielibyśmy wejść trochę głębiej w zagadnienie obsługi błędów, ale nie miejsce i czas na to. Warto jednak wspomnieć o kilku możliwościach, choćby i bez wchodzenia w szczegóły.
Przede wszystkim powinniśmy zadbać, żeby funkcja, którą przypisujemy do zmiennej zawsze zwracała wartość. Dobrze się też dodatkowo zabezpieczyć i sprawdzić wynik funkcji przed przypisaniem.
Podsumowując:
- zawsze inicjalizuj zmienne w momencie ich deklaracji,
- sprawdzaj istnienie właściwości obiektu przed ich użyciem,
- używaj ścisłego porównania === zamiast luźnego ==,
- bądź świadomy przypadków, w których funkcje mogą zwrócić undefined, i zarządzaj nimi odpowiednio.
Nie jest to jeszcze koniec dzisiejszej przygody z błędami - w końcu do omówienia mamy także zmienną typu null 😉
Zero-niezero, czyli o null słów kilka
Na początku bardzo ogólnikowo i łopatologicznie opowiedzieliśmy sobie o zmiennej typu null. Czas się pochylić nad nią dokładniej.
Zmienna typu null jest używana do celowego przypisania wartości oznaczającej "brak wartości" lub "pusty" stan. Jest to typ, który reprezentuje nieistniejący lub nieobecny obiekt.
W przeciwieństwie do undefined, który jest automatycznie przypisywany uprzednio zadeklarowanym, choć nie zainicjalizowanym zmiennym,, null jest przypisywany ręcznie przez programistę, aby jednoznacznie wskazać, że zmienna ma być pusta.
const user = null; // Użytkownik nie jest zalogowany
Tutaj zmiennej user celowo przypisano wartość null, aby wskazać, że w danym momencie nie ma użytkownika, który byłby zalogowany. Jest to dużo bezpieczniejsze rozwiązanie, niż praca na nieistniejącej zmiennej user lub zmiennej zadeklarowanej bez wartości.
Różnice między null a undefined
Na tym etapie mamy już chyba pełną świadomość, że pomimo kilku podobieństw zmienne typu null i undefined są od siebie mocno różne i mają zdecydowanie inne zastosowania.
Pokusiłbym się o wyszczególnienie dwóch obszarów, gdzie te różnice - przynajmniej moim zdaniem - są najistotniejsze.
- inicjalizacja: undefined jest wartością domyślną przypisaną do zmiennej, która została zadeklarowana, ale nie zainicjalizowana. Z kolei null musi być przypisany ręcznie, aby zaznaczyć brak wartości,
- cel użycia: undefined zazwyczaj wskazuje na błąd lub przypadkowe braki w inicjalizacji, natomiast null jest stosowany świadomie do oznaczenia pustego stanu.
Zero błędów, czy błędy z null
Zmienna nullpotrafi być bardzo użyteczna. Jednak może prowadzić do pewnych problemów, szczególnie jeśli nie jest odpowiednio zarządzana. Spójrzmy, jakie potencjalne błędy związane ze zmienną null możemy spotkać w kodzie oraz jak sobie z nimi radzić.
Niezgodność typów w operacjach
Null jest uważany za wartość falsy (czyli wartość, która w kontekście logicznym jest traktowana jako false), co może prowadzić do błędów w operacjach logicznych lub porównawczych, szczególnie gdy spodziewamy się wartości liczbowej lub obiektu.
const value = null;
if (value) {
console.log("Wartość jest prawdziwa");
} else {
console.log("Wartość jest falsy"); // Wartość jest falsy
}
Tutaj null jest traktowany jako wartość falsy, co może być nieoczekiwane, jeśli zapomnimy, że jest to wskaźnik na brak wartości.
Problemy z metodami i właściwościami
Przypisanie null do zmiennej, która powinna zawierać obiekt, może prowadzić do błędów, jeśli spróbujemy uzyskać dostęp do właściwości lub metod tego obiektu. W takim przypadku próba odwołania się do nieistniejącej właściwości lub metody spowoduje błąd typu TypeError.
const user = null;
console.log(user.name); // TypeError: Cannot read properties of null
W tym przykładzie zmienna user została ustawiona na null, ale później próbowano uzyskać dostęp do właściwości name, co powoduje błąd.
Niedostateczna walidacja wartości
Jeśli zapomnimy zweryfikować, czy zmienna zawierająca null jest poprawnie inicjalizowana lub używana, może to prowadzić do błędów, szczególnie w dużych aplikacjach, gdzie śledzenie wartości wszystkich zmiennych jest trudniejsze.
function getUserData() {
return null;
}
const userData = getUserData();
console.log(userData.name); // TypeError: Cannot read properties of null
W takim przypadku funkcja getUserData zwraca null, ale nie sprawdziliśmy tej wartości przed próbą odwołania się do właściwości name.
Remedium na to jest sprawdzenie, czy zmienne, które mogą zawierać null, są poprawnie zainicjalizowane, zanim spróbujemy odwołać się do ich właściwości lub metod. Warto używać konstrukcji warunkowych do sprawdzenia, czy wartością zmiennej jest null, zanim podejmie się dalsze działania.
if (user !== null) {
console.log(user.name);
} else {
console.log("Brak danych użytkownika");
}
Innym sposobem jest zastosowanie wartości domyślnych. Można je ustawiać dla zmiennych, aby uniknąć problemów związanych z null. Na przykład, zamiast przypisywać null, można przypisać pusty obiekt lub inne wartości, które będą mniej narażone na wywołanie błędów.
const user = user || {};
console.log(user.name); // undefined, ale bez błędu
Warto skorzystać z Optional Chaining (?.), który pozwala bezpiecznie odwoływać się do właściwości i metod nawet wtedy, gdy obiekt ma wartość null lub undefined.
console.log(user?.name); // undefined, brak błędu
Na sam koniec warto też pamiętać o testach jednostkowych. Regularne testowanie kodu, szczególnie w obszarach, gdzie spodziewamy się wystąpienia null, pomoże wychwycić błędy i zapewnić, że aplikacja działa poprawnie we wszystkich przypadkach. Jednak w tym momencie nie będziemy się na nich za bardzo skupiać - choć gorąco polecam zacząć poznawać narzędzia do testowania jak najszybciej.
Ogniska już dogasa blask, czyli kilka wniosków na zakończenie
Nie ukrywam, że powyższy tekst jedynie prześlizgnął się po temacie null i undefined. le spokojnie - każdy przypadek, kiedy będziecie spotykać te typy w kodzie, pozwoli je coraz lepiej rozumieć.
Tymczasem kilka wniosków odnośnie typów null i undefined na zakończenie:
Różnice Semantyczne
Undefined reprezentuje brak inicjalizacji lub nieistniejącą wartość, natomiast null jest celowym (co za każdym razem podkreślam) wskaźnikiem na "brak wartości" lub "pusty stan". Undefined jest przypisywany automatycznie, podczas gdy null wymaga ręcznego przypisania. I tak - oczywiście, że undefined także można przypisać ręcznie, ale trzeba mieć ku temu naprawdę dobry powód.
Zastosowanie w Kodzie
Używajcie undefined jako sygnału, że coś nie zostało poprawnie zainicjalizowane lub funkcja nie zwróciła żadnej wartości. Natomiast null stosujcie wtedy, gdy chcecie jawnie wskazać brak wartości lub pusty stan w zmiennej, obiekcie lub tablicy.
Typowanie
Oba typy (null i undefined) są częścią prymitywnych typów danych, ale ich zastosowanie ma różne implikacje logiczne i praktyczne, szczególnie w operacjach porównywania.
Garść porad, jak nie wpaść w pułapkę
-
Zawsze inicjalizuj zmienne: unikajcie sytuacji, w których zmienne są deklarowane bez przypisania wartości. Dzięki temu unikniecie nieoczekiwanych wyników związanych z undefined.
-
Używaj null świadomie: jeśli zamierzacie pozostawić zmienną "pustą", użyjcie null, aby jasno zaznaczyć, że brak wartości jest zamierzony.
-
Stosuj ścisłe porównania: używajcie operatorów === i !== zamiast == i !=, aby wyraźnie zróżnicować null i undefined, co pozwoli uniknąć niejednoznaczności w kodzie.
-
Sprawdzaj istnienie właściwości: przed operowaniem na właściwościach obiektów lub tablic sprawdźcie, czy właściwość istnieje, aby uniknąć błędów związanych z undefined.
-
Obsługuj zwracane wartości z funkcji: zawsze oczekujcie i odpowiednio obsługujcie możliwe przypadki, w których funkcja może zwrócić undefined, zwłaszcza w sytuacjach, gdzie może to prowadzić do błędów.
Pamiętajcie - trening czyni mistrza! Tworzenie małych aplikacji lub skryptów, które wymagają świadomego zarządzania stanem zmiennych, pomoże w lepszym zrozumieniu, kiedy i jak używać null i undefined w praktyce.
Dobrze. W ten oto sposób zakończyliśmy przegląd prymitywnych typów danych w JavaScript. W następnej części zacznie się już jazda bez trzymanki, będzie to bowiem wstęp do typu złożonego, jakim jest Object.
Tymczasem tradycyjnie zapraszam na mojego twixera oraz do lektury MDN (null oraz undefined).
Do następnego!