Специальные значения в JavaScript — null, undefined и NaN. Каждое означает отсутствие нормального значения, но по-разному:

  • null — явно «значение есть, оно — ничего». Программист или API пишет null сознательно: «здесь нет данных, и мы это знаем».
  • undefined — «значение не задано». Переменная объявлена, но без присваивания. Поле объекта не существует. Функция не вернула return-значение. Аргумент не передали.
  • NaN (Not a Number) — «вычисление не дало числового результата». Делил строку на число, передал в parseInt не-число, умножил undefined на 2.
let a;                          // undefined  (переменная без значения)
let b = null;                   // null       (явно "пусто")
let c = Number("привет");       // NaN        (числовое преобразование провалилось)

let obj = { name: 'Anna' };
console.log(obj.age);           // undefined  (нет такого поля)

function noReturn() {}
console.log(noReturn());        // undefined  (функция без return)

На первый взгляд null и undefined кажутся одинаковыми — оба означают «пусто». Но язык различает их в десятках мест:

// Тип:
typeof null         // "object"   — историческая ошибка стандарта, не лечится
typeof undefined    // "undefined"

// Числовая конверсия:
Number(null)        // 0
Number(undefined)   // NaN

// Сравнение между собой:
null == undefined   // true     (== делает их равными)
null === undefined  // false    (=== — строго разные)

// Default-параметры функций:
function f(x = 5) { return x; }
f()                 // 5  (undefined → подставляется дефолт)
f(undefined)        // 5  (то же)
f(null)             // null  (!) — null дефолт не активирует

// JSON.stringify:
JSON.stringify({ a: null, b: undefined })
// '{"a":null}'      — undefined-поле пропало, null остался

Каждое из трёх спецзначений рано или поздно приходится превращать во что-то более «нормальное»: подставить дефолт для рендера, преобразовать в строку для лога, исключить из вычислений. Логика всегда одна: проверить, что значение — одно из трёх спецзначений, и если да — заменить на «безопасный» аналог нужного типа.

Универсальная проверка одной строкой — x == null (нестрогое сравнение): ловит и null, и undefined, но не 0/"". Для NaN отдельно: Number.isNaN(x).

Преобразование специальных значений в строку

В строку null, undefined и NaN превращаются буквально — в текст с их именем. Это бывает удобно для логов и отладки, но почти всегда плохо для UI: видеть в интерфейсе подпись «Цена: undefined» пользователю не нужно.

String(null)         // "null"
String(undefined)    // "undefined"
String(NaN)          // "NaN"

// Шаблонный литерал делает то же:
`${null}`            // "null"
`${undefined}`       // "undefined"
`${NaN}`             // "NaN"

// .toString() на null/undefined упадёт:
(null).toString()        // TypeError: Cannot read properties of null
(undefined).toString()   // TypeError: Cannot read properties of undefined
(NaN).toString()         // "NaN"  ← у NaN метод есть, потому что typeof NaN === "number"

Для UI обычно нужна осмысленная замена — пустая строка или плейсхолдер. Делается тернарником или nullish coalescing:

let value = null;

// Способ 1: явная проверка
let display = (value == null) ? '' : String(value);
console.log(display);   // ""

// Способ 2: nullish coalescing
display = value ?? '—';
console.log(display);   // "—"

// Способ 3: классическая идиома из старого кода
let a = null;
if (typeof a === 'undefined' || a === null) {
    a = '';
}
console.log(a);         // ""

Третий способ — самый многословный, но он подсвечивает идею: проверь спецзначение, замени осмысленным дефолтом. Современный код почти всегда сводит это к value ?? ''. Если требуется обрабатывать ещё и NaN, добавляй отдельную проверку: Number.isNaN(x) ? '' : x.

Преобразование специальных значений в число

Здесь поведение трёх спецзначений расходится сильнее всего. null становится нулём, undefinedNaN, а NaN остаётся самим собой. Эта разница — один из самых частых источников багов в арифметике.

Number(null)         // 0    (внезапно)
Number(undefined)    // NaN
Number(NaN)          // NaN

// Унарный плюс — короткая форма Number():
+null                // 0
+undefined           // NaN
+NaN                 // NaN

// В арифметике:
null + 1             // 1      (null → 0, потом + 1)
undefined + 1        // NaN    (undefined → NaN, потом NaN + 1 → NaN)
null * 5             // 0
undefined * 5        // NaN

Запомнить разницу просто: null — это «значение есть, оно равно ничему», и язык считает это «ничем» число 0. undefined — это «значения вообще нет», и его в число превратить нельзя, поэтому NaN.

Чтобы спецзначение не «заражало» вычисления, его нужно нормализовать до того, как оно попадёт в формулу:

// плохо: undefined из необязательного поля даст NaN на выходе
let total = price * quantity;       // если quantity = undefined → NaN

// хорошо: подставляем 0 заранее
let total = (price ?? 0) * (quantity ?? 0);

// классическая форма для тех же целей:
let a = undefined;
if (typeof a === 'undefined' || a === null) {
    a = 0;
}
console.log(a);          // 0

// для NaN — отдельная проверка
let calc = parseFloat(userInput);
if (Number.isNaN(calc)) {
    calc = 0;
}

Главная подсказка для отладки: если в цепочке вычислений на выходе NaN, где-то наверху лежит undefined или результат провального parseInt. NaN «заражает» всю арифметику дальше по цепочке: NaN + 1 === NaN, NaN * 0 === NaN.

Преобразование специальных значений в булевое

Самый предсказуемый случай: все три спецзначения — falsy. Boolean() возвращает false на каждое, и в условиях if они эквивалентны «нет данных».

Boolean(null)        // false
Boolean(undefined)   // false
Boolean(NaN)         // false

// !! — короткая идиома:
!!null               // false
!!undefined          // false
!!NaN                // false

// в условиях:
let user;
if (user) {
    console.log('не зайдём — undefined falsy');
}

let result = NaN;
if (result) {
    console.log('тоже не зайдём — NaN falsy');
}

Удобство в том, что одна проверка if (value) отсекает все три случая. Но если 0 или пустая строка — валидные значения в твоей логике, такая проверка будет ловить и их.

let count = 0;

// ловит и null/undefined/NaN, и 0/""
if (!count) {
    console.log('нет данных');     // зайдёт, хотя 0 — валидно
}

// ловит только null/undefined (без 0 и "")
if (count == null) {
    console.log('правда нет данных');   // не зайдёт
}

// ловит null/undefined/NaN, но не 0/""
if (count == null || Number.isNaN(count)) {
    console.log('явно спецзначение');
}

// классическая форма из старого кода:
let a;
if (typeof a === 'undefined' || a === null) {
    a = false;
}

Запись x == null с двойным равенством — редкий случай, когда нестрогое сравнение читабельнее: ловит ровно null и undefined, и больше ничего.

Преобразование специальных значений в массив

В массив спецзначение превращают, чтобы дальше можно было использовать стандартные методы (.map, .filter, .forEach) без падений. Обычно нужен либо пустой [] (когда значения нет), либо массив-обёртка [value].

// Способ 1: подставить пустой массив через ??
let items = data ?? [];      // если data null/undefined → []
items.forEach(item => render(item));   // не упадёт

// Способ 2: обернуть значение, даже если оно null/undefined
let arr = [null];            // [null]   — массив из одного элемента
let arr2 = [undefined];      // [undefined]
let arr3 = [NaN];            // [NaN]

// классическая форма:
let a = null;
if (typeof a === 'undefined' || a === null) {
    a = [];
}
console.log(a);              // []

Современный способ — почти всегда value ?? []. Эту запись часто встретишь в реактивных компонентах: const tags = props.tags ?? [] — если родитель не передал теги, рендерим пустой список вместо падения.

Если массив нужен из любого значения (не только спецзначения), используется Array.from() или [].concat():

// "Нормализация" значения в массив: либо массив, либо одно значение в массиве
function toArray(value) {
    if (value == null) return [];
    return Array.isArray(value) ? value : [value];
}

toArray(null)         // []
toArray(undefined)    // []
toArray(42)           // [42]
toArray([1, 2, 3])    // [1, 2, 3]
toArray("hello")      // ["hello"]   — строки не разделяются!

Такая функция полезна для API, который может вернуть либо одно значение, либо массив — стандартизирует на «всегда массив», и весь дальнейший код работает однообразно.

Преобразование специальных значений в объект

Самый редкий случай. Обычно нужен либо пустой {} для безопасного destructuring, либо объект-обёртка (для интероп со старым кодом). У встроенного Object() поведение специфическое:

Object(null)          // {}    (пустой объект)
Object(undefined)     // {}    (то же)
Object(NaN)           // Number {NaN}   (объект-обёртка над числом NaN)

// современный способ — через ??
let config = userConfig ?? {};

// классическая форма:
let a;
if (typeof a === 'undefined' || a === null) {
    a = {};
}
console.log(a);       // {}

Главный кейс — безопасный destructuring. Когда из null/undefined пытаешься достать поле, JavaScript падает с TypeError. Подставив пустой объект, получаем undefined для каждого поля — падения нет.

// упадёт, если response.user — null:
const { name, email } = response.user;
// TypeError: Cannot destructure property 'name' of 'response.user' as it is null

// безопасно:
const { name, email } = response.user ?? {};
// name = undefined, email = undefined

// то же с default-значениями:
const { name = 'Аноним', email = 'нет почты' } = response.user ?? {};

В сочетании с optional chaining (?.) и nullish coalescing (??) это даёт мощный паттерн для работы с непредсказуемыми API-ответами: const value = response?.data?.user?.name ?? 'guest'.

Подводные камни

typeof null === "object". Историческая ошибка стандарта — null сообщает свой тип как "object". Чтобы отличить null от настоящего объекта, делается отдельная проверка: x !== null && typeof x === "object".

NaN не равен сам себе. NaN === NaNfalse. Это «фича» стандарта IEEE 754 (формат чисел с плавающей точкой). Проверка на NaN делается через Number.isNaN(x) — именно так, не через глобальный isNaN() (тот сначала кастит аргумент в число и может ошибочно говорить true для строк вроде isNaN("hello")).

JSON.stringify отбрасывает undefined. Если значение поля undefined, поле выпадает из итогового JSON. null остаётся. Если бэку нужен явный сигнал «поле есть, значения нет», всегда используй null, не undefined.

Default-параметры и null. function f(x = 5) — дефолт сработает только для undefined (включая «не передали аргумент»). Если передать null, дефолт не подставится — x станет null. Это часто ловит врасплох.

?? срабатывает только на null и undefined. В отличие от ||, оператор ?? не считает 0 или пустую строку поводом для подмены. 0 ?? 5 — это 0, не 5. Если ноль или пустая строка — валидные значения, всегда ??, не ||.

undefined можно переопределить (раньше). До ES5 undefined был обычной переменной, которую можно было переписать (undefined = 42). В современных движках это запрещено, но в очень старом коде встречается идиома void 0 — гарантированный undefined на случай переопределённой глобальной.