JavaScript jest dynamicznie typowanym językiem, który oferuje dwa sposoby reprezentacji braku wartości: null i undefined. Choć na pierwszy rzut oka mogą wydawać się podobne, pełnią zupełnie różne role w ekosystemie języka.
Undefined jest automatycznie przypisywane przez interpreter do zmiennych zadeklarowanych, ale nie zainicjalizowanych, oraz do funkcji, które nie zwracają żadnej wartości. Null natomiast jest celowo przypisywaną wartością, którą programista używa do jawnego wskazania braku wartości lub pustej referencji.
Zrozumienie precyzyjnych różnic między tymi dwoma typami jest kluczowe dla pisania bezpiecznego i przewidywalnego kodu.
Fundamentalne definicje i naturalne pochodzenie
Czym jest undefined
Undefined jest pierwotnym typem danych w JavaScript, który ma specjalne znaczenie w systemie typów języka. Jest to wartość, którą interpreter automatycznie przypisuje zmiennym zadeklarowanym za pomocą var, let lub const, ale bez wartości podczas inicjalizacji.
Warto rozróżnić sam typ i wartość literałową. Undefined jest zarówno typem danych, jak i konkretną wartością, którą interpreter zwraca w określonych sytuacjach. Konkretnie, undefined pojawia się w sytuacjach takich jak:
- zmienna jest deklarowana, ale nie inicjalizowana,
- funkcja nie zawiera instrukcji
returnlub zwracareturnbez wartości, - próba odczytu nieistniejącej właściwości obiektu,
- próba odczytu elementu tablicy spoza jej granic,
- pominięty parametr funkcji podczas wywołania.
Warte odnotowania: undefined jest właściwością obiektu globalnego (dostępne jako globalThis.undefined). W przeciwieństwie do null, undefined nie jest zarezerwowanym słowem kluczowym, co teoretycznie umożliwia jego przysłonięcie w lokalnym zakresie. To zdecydowanie odradzana praktyka, utrudniająca utrzymanie i czytanie kodu.
Czym jest null
Null jest również pierwotnym typem danych i stanowi specjalny przypadek w systemie typów. W przeciwieństwie do undefined nigdy nie jest automatycznie przypisywany przez interpreter. To wartość, którą programista musi jawnie przypisać, aby wskazać intencjonalny brak wartości lub pustą referencję.
Koncepcyjnie null można rozumieć jako „puste pudełko” – kontener, który istnieje i jest dostępny, ale aktualnie niczego nie zawiera. Undefined można zaś porównać do braku samego pudełka – zmiennej, która nie ma jeszcze przypisanej wartości.
Historycznie null pochodzi z koncepcji wskaźnika zerowego (null pointer) znanej z języków takich jak C. W JavaScript null jest zarezerwowanym słowem kluczowym, więc nie można go przysłonić ani przedefiniować.
System typów i reprezentacja binarna
Dziwny przypadek typeof null
Jednym z najbardziej znanych „dziwactw” w JavaScript jest fakt, że typeof null zwraca "object" zamiast "null". To wynik historycznego błędu w implementacji, którego nie naprawiono ze względu na kompatybilność wsteczną.
We wczesnej implementacji JavaScript wartości były reprezentowane 32-bitowymi jednostkami, gdzie pierwsze trzy bity zawierały tag typu, a pozostałe bity zawierały wartość lub adres w pamięci. Dla obiektów tag typu wynosił 000. Null zakodowano jako same zera (0x00000000), więc jego pierwsze trzy bity też były 000. Dlatego operator typeof interpretuje null jako "object".
Mimo że to błąd, jego zmiana złamałaby ogromne ilości istniejącego kodu, więc pozostawiono dotychczasowe zachowanie. Oto kilka przykładów:
typeof undefined; // "undefined"
typeof null; // "object" – znany błąd
typeof 42; // "number"
typeof "hello"; // "string"
typeof true; // "boolean"
Typ undefined
Typ undefined jest prosty i konsekwentny – operator typeof zwraca dokładnie "undefined" dla wartości undefined. Przykład:
let x;
console.log(typeof x); // "undefined"
console.log(x); // undefined
Deklaracja, inicjalizacja i przypisanie
Hoisting i domyślne inicjalizacje
Aby w pełni zrozumieć różnice między undefined i null, warto przeanalizować, jak JavaScript obsługuje deklaracje i hoisting. Hoisting to mechanizm, w którym JavaScript „podnosi” deklaracje na początek zakresu podczas etapu kompilacji.
Dla zmiennych deklarowanych za pomocą var deklaracja jest hoistowana, a zmienna otrzymuje domyślnie undefined. Oznacza to, że można odwołać się do niej przed linią deklaracji i otrzymać undefined zamiast błędu ReferenceError. Przykład:
console.log(x); // undefined
var x = 5;
console.log(x); // 5
W tym przykładzie x jest hoistowane na początek zakresu z wartością undefined, a następnie aktualizowane do 5. Z kolei zmienne let i const również są hoistowane, ale pozostają w Temporal Dead Zone (TDZ) aż do faktycznej inicjalizacji.
Null nigdy nie jest przypisywane domyślnie – aby przypisać null, trzeba to zrobić jawnie.
Różne scenariusze pojawienia się undefined
Undefined pojawia się w wielu sytuacjach, będąc wynikiem automatycznych reguł silnika. Oto praktyczne przykłady:
Gdy funkcja nie zawiera return lub zawiera return bez wyrażenia, zwraca undefined:
function testFunction() {
// brak instrukcji return
}
console.log(testFunction()); // undefined
function anotherFunction() {
return; // return bez wartości
}
console.log(anotherFunction()); // undefined
Gdy próbujesz odczytać nieistniejącą właściwość obiektu, otrzymasz undefined (zamiast błędu). Przykład:
const person = { name: "John" };
console.log(person.age); // undefined – właściwość nie istnieje
Gdy pominiesz parametr funkcji przy wywołaniu, jego wartość to undefined. Przykład:
function greet(name, age) {
console.log(name); // "John"
console.log(age); // undefined
}
greet("John");
Gdy odczytasz element tablicy poza jej zakresem, otrzymasz undefined. Przykład:
const array = [1, 2, 3];
console.log(array[5]); // undefined – indeks nie istnieje
Jawne przypisanie null
Null musi być zawsze przypisane jawnie – nie istnieje automatyczny mechanizm robiący to „za ciebie”. Przykład:
let value = null; // jawne przypisanie
console.log(value); // null
Widząc undefined, zwykle wiesz, że czegoś nie zainicjalizowano lub funkcja niczego nie zwróciła. Widząc null – że był to świadomy wybór programisty.
Porównanie i operatory równości
Luźna równość vs ścisła równość
JavaScript oferuje dwa operatory porównania: luźną równość (==) i ścisłą równość (===). Zachowują się one różnie dla null i undefined. Przykłady:
console.log(null === undefined); // false – różne typy
console.log(null === null); // true
console.log(undefined === undefined); // true
console.log(null == undefined); // true – luźna równość
console.log(null == null); // true
console.log(undefined == undefined); // true
Ogólna rekomendacja: używaj ścisłej równości (===), chyba że masz bardzo konkretny powód, by skorzystać z ==.
Null i undefined – szybkie porównanie
Dla syntetycznego przeglądu najważniejszych różnic pomoże poniższa tabela:
| Aspekt | undefined | null |
|---|---|---|
| Pochodzenie | ustawiane automatycznie przez silnik | przypisywane jawnie przez programistę |
| typeof | „undefined” | „object” (historyczny błąd) |
| Wartość falsy | tak | tak |
| JSON.stringify | pomijane | zachowywane |
| Konwersja liczbowa | NaN | 0 |
| Równość luźna | równe null (undefined == null → true) | równe undefined (null == undefined → true) |
| Typowe użycie | brak inicjalizacji, brak zwrotu, brak parametru | intencjonalny brak wartości/referencji |
Specjalny przypadek – luźne sprawdzenie null
Istnieje jeden powszechnie akceptowany wyjątek. Porównanie value == null sprawdza w jednym warunku, czy wartość jest null lub undefined. Przykład:
if (value == null) {
// value jest null lub undefined
}
Dla porównania równoważna, ale dłuższa forma wygląda tak:
if (value === null || value === undefined) {
// value jest null lub undefined
}
Wartości falsy i konwersja typów
Wartości falsy w JavaScript
Zarówno null, jak i undefined są wartościami falsy – po konwersji do typu logicznego stają się false. Do wartości falsy w JavaScript należą:
- undefined,
- null,
- NaN,
- 0,
- pusty łańcuch „”,
- false.
Oto przykłady konwersji do boolean:
!!null; // false
!!undefined; // false
!!0; // false
!!NaN; // false
!!false; // false
!!""; // false
!!"hello"; // true
!!1; // true
!!{}; // true
Ma to znaczenie dla instrukcji warunkowych – null lub undefined w if będą traktowane jako false. Przykład:
const value = null;
if (value) {
console.log("Ten blok się nie wykona");
} else {
console.log("Ten blok się wykona");
}
Zachowanie w operacjach arytmetycznych
Null i undefined zachowują się inaczej w arytmetyce: null konwertuje się do 0, a undefined do NaN. Przykłady:
console.log(null + 1); // 1 – null konwertuje się do 0
console.log(undefined + 1); // NaN – undefined konwertuje się do NaN
console.log(null * 2); // 0
console.log(undefined * 2); // NaN
To częste źródło subtelnych błędów – niezamierzone undefined w obliczeniach szybko propaguje się jako NaN.
Praktyczne scenariusze i zastosowania
Nieistniejące właściwości obiektów
Typowy przypadek undefined to odczyt nieistniejącej właściwości obiektu – zamiast błędu otrzymujesz undefined. Przykład:
const user = { name: "Alice", email: "[email protected]" };
console.log(user.name); // "Alice"
console.log(user.phone); // undefined – właściwość nie istnieje
console.log(user.address); // undefined – właściwość nie istnieje
Null bywa używane celowo, np. w odpowiedziach JSON, by zaznaczyć, że pole istnieje, ale obecnie nie ma wartości: {"user": null}.
Serializacja i JSON
Podczas serializacji do JSON istnieje ważna różnica: JSON.stringify pomija wartości undefined, ale zachowuje wartości null. Przykład:
const obj = { name: "John", age: undefined, city: null, phone: undefined };
console.log(JSON.stringify(obj)); // {"name":"John","city":null}
// age i phone zostały pominięte, ponieważ są undefined
Jeśli chcesz wysłać informację „pole puste”, użyj null, a nie undefined.
Parametry funkcji i domyślne wartości
Parametry, których nie przekazano przy wywołaniu, mają wartość undefined. ES6 wprowadził domyślne wartości parametrów, używane tylko wtedy, gdy parametr jest undefined (nie zaś gdy jest inną wartością falsy, np. 0 lub ""). Przykłady:
function greet(name, age) {
console.log(`Cześć, mam na imię ${name}`);
console.log(`Mam ${age} lat`);
}
greet("Alice"); // age jest undefined
// Output:
// Cześć, mam na imię Alice
// Mam undefined lat
function greetWithDefaults(name = "John", age = 0) {
console.log(`Cześć, mam na imię ${name}`);
console.log(`Mam ${age} lat`);
}
greetWithDefaults(); // użyje wartości domyślnych
greetWithDefaults("Alice", 0); // 0 nie jest undefined, więc domyślna wartość nie zostanie użyta
Zwracanie wartości z funkcji
Funkcje bez return lub z return bez wartości zwracają undefined. Przykłady:
function noReturn() {
console.log("robię coś");
}
function earlyReturn() {
if (true) {
return; // brak wartości
}
return "value";
}
console.log(noReturn()); // undefined
console.log(earlyReturn()); // undefined
Nowoczesne techniki obsługi null i undefined
Operator nullish coalescing (??)
ES2020 wprowadził operator nullish coalescing (??), precyzyjniejszy niż || w obsłudze „braków”. ?? zwraca prawą stronę tylko wtedy, gdy lewa jest nullish (null lub undefined), a nie w przypadku innych wartości falsy. Przykłady:
const value1 = 0 || 10; // 10 (bo 0 jest falsy)
const value2 = 0 ?? 10; // 0 (bo 0 nie jest nullish)
const text1 = "" || "domyślny"; // "domyślny" (pusty string jest falsy)
const text2 = "" ?? "domyślny"; // "" (pusty string nie jest nullish)
const count = userCount ?? 0; // użyje userCount nawet, gdy wynosi 0
Operator optional chaining (?.)
ES2020 dodał także operator optional chaining (?.), który pozwala bezpiecznie odczytywać zagnieżdżone właściwości – jeśli na drodze wystąpi wartość nullish, wynik to undefined, a nie błąd. Przykłady:
const user = { profile: { address: { street: "Main St" } } };
console.log(user.profile?.address?.street); // "Main St"
console.log(user.profile?.phone?.number); // undefined (phone nie istnieje)
console.log(user.settings?.theme); // undefined
Przed wprowadzeniem ?. należało pisać defensywny kod, np. tak:
const street = user && user.profile && user.profile.address && user.profile.address.street;
Rekomendacje i najlepsze praktyki
Kiedy używać undefined
Pozwól, by undefined działało automatycznie i nie przypisuj go jawnie. Widząc undefined, łatwiej zidentyfikować niezainicjalizowaną zmienną lub brak zwrotu z funkcji. Naturalne przypadki występowania undefined to:
- parametry funkcji, które nie zostały dostarczone,
- funkcje, które nie zwracają wartości,
- właściwości obiektów, które nie istnieją,
- elementy tablic poza granicami.
Oto czytelny wzorzec zachowania:
// Dobrze – pozwól undefined działać automatycznie
function processData(data) {
if (data === undefined) {
// obsługa braku danych
}
}
// Unikaj – jawne przypisanie undefined
const variable = undefined; // nie rób tak
Kiedy używać null
Null przypisuj jawnie, gdy chcesz wskazać intencjonalny brak wartości teraz i ewentualną obecność wartości w przyszłości. Przykłady:
// Dobrze – jawne null dla „braku wartości”
let userPreferences = null;
// później, np. po załadowaniu z API
userPreferences = fetchUserPreferences();
// Dobrze – null w obiekcie, by zasygnalizować pustkę pola
const user = {
name: "John",
middleName: null, // celowo brak drugiego imienia
email: "[email protected]"
};
W Node.js i w API opartych na callbackach przyjęto konwencję callback(error, result), gdzie pierwszy argument to null, jeśli nie wystąpił błąd.
Sprawdzanie obu wartości jednocześnie
Choć zwykle preferujemy ===, idiom value == null jest czytelny i bezpieczny do sprawdzenia jednocześnie null lub undefined. Nowocześnie można też użyć ??. Przykłady:
// Czytelnie – sprawdź null lub undefined
if (config == null) {
config = getDefaultConfig();
}
// Nowocześnie – użyj nullish coalescing
config ??= getDefaultConfig();
Typowanie w TypeScript
W TypeScript można jawnie określać zmienne jako dopuszczające null lub undefined, co umożliwia statyczną analizę i wcześniejsze wykrycie błędów. Przykład:
// Jawne typy z możliwością null/undefined
const name: string | null = null;
const age: number | undefined = undefined;
// Typy nienullowalne
const email: string = "[email protected]"; // nie może być null ani undefined
Zaawansowane scenariusze i przypadki brzegowe
Destrukturyzacja i domyślne wartości
Próba destrukturyzacji null lub undefined rzuci TypeError. Domyślne wartości w destrukturyzacji są używane, gdy właściwość jest undefined, ale nie gdy jest null. Przykłady:
// To zgłosi TypeError
const { name } = null; // Error: Cannot destructure property of null
// Działa – domyślne wartości dla właściwości undefined
const { name: displayName = "Unknown" } = { name: undefined };
console.log(displayName); // "Unknown"
// Domyślna wartość NIE jest użyta, jeśli właściwość to null
const { email = "[email protected]" } = { email: null };
console.log(email); // null (nie wartość domyślna)
Operator spread
Dla object spread wartości null i undefined są ignorowane. Dla array spread próba rozproszenia null lub undefined powoduje TypeError (nie są iterowalne). Przykłady:
// Object spread – null i undefined są ignorowane
const obj = { a: 1, ...null, ...undefined, b: 2 };
console.log(obj); // { a: 1, b: 2 }
// Array spread – rzuca TypeError
const arr = [...null]; // TypeError
const arr2 = [...undefined]; // TypeError
Array.find() i zwracane wartości
Array.prototype.find() zwraca pierwszy dopasowany element lub undefined, jeśli go brak. To bywa mylące, gdy spodziewane wartości mogą być falsy (np. 0). Przykład:
const numbers = [0, 1, 2, 3, 4, 5];
const result = numbers.find(n => n === 0);
console.log(result); // 0 – ale to wartość falsy!
if (result) {
console.log("Znaleziono!");
} else {
console.log("Nie znaleziono!"); // To wykona się błędnie!
}
// Lepiej – sprawdź jawnie undefined
if (result !== undefined) {
console.log("Znaleziono!");
}
Hoisting zmiennych z let i const
Zmiennych deklarowanych przez let i const nie można użyć przed inicjalizacją – znajdują się w TDZ i próba odwołania kończy się ReferenceError. To inne zachowanie niż przy var, gdzie zmienna istnieje od początku zakresu z wartością undefined. Przykład:
console.log(x); // ReferenceError: Cannot access 'x' before initialization
let x = 5;
// Kontrast z var
console.log(y); // undefined
var y = 5;