Back

Странная жизнь NaN в JavaScript

Странная жизнь NaN в JavaScript

Вы наверняка сталкивались с этим: вычисление возвращает NaN, и внезапно весь ваш конвейер обработки данных выдаёт мусор. Или ещё хуже — сравнение, которое должно было обнаружить проблему, молча проваливается, потому что NaN === NaN возвращает false. Понимание особенностей NaN в JavaScript — это не опция, а необходимость для написания надёжного кода, работающего с числами.

В этой статье объясняется, почему NaN ведёт себя именно так, где он обычно появляется и как правильно его обнаруживать с помощью современных методов.

Ключевые выводы

  • NaN — это корректный числовой тип согласно IEEE 754, представляющий неопределённые или непредставимые математические результаты.
  • NaN никогда не равен самому себе — используйте Number.isNaN() или Object.is() для надёжного обнаружения.
  • Глобальная функция isNaN() сначала приводит аргументы к числу, что вызывает ложные срабатывания; избегайте её.
  • JSON-сериализация молча преобразует NaN в null, в то время как structuredClone() сохраняет его.
  • Валидируйте числовые входные данные на границах, чтобы предотвратить распространение NaN через вычисления.

Почему существует NaN и почему typeof возвращает “number”

NaN расшифровывается как “Not a Number” (не число), но typeof NaN возвращает "number". Это не баг JavaScript — это сделано специально.

Стандарт IEEE 754 для чисел с плавающей точкой, которому следует JavaScript, определяет NaN как специальное значение внутри числового типа. Он представляет результат неопределённых или непредставимых математических операций. Думайте о нём как о заполнителе, который говорит: “это вычисление не дало корректного числового результата”.

console.log(typeof NaN) // "number"

NaN существует потому, что числовым операциям нужен способ сигнализировать о сбое без выбрасывания исключений. Деление на ноль с целыми числами привело бы к краху во многих языках, но 0 / 0 возвращает NaN и позволяет вашей программе продолжить работу.

Распространённые источники NaN в JavaScript

NaN появляется чаще, чем вы могли бы ожидать:

// Неудачный парсинг
Number("hello")        // NaN
parseInt("abc")        // NaN

// Некорректные математические операции
0 / 0                  // NaN
Math.sqrt(-1)          // NaN
Infinity - Infinity    // NaN

// Операции с undefined
undefined * 5          // NaN

Данные из форм — частый виновник. Когда parseFloat(userInput) встречает нечисловой текст, NaN молча проникает в ваши вычисления и распространяется через каждую последующую операцию.

Правила сравнения NaN: почему NaN !== NaN

Вот поведение, которое сбивает с толку большинство разработчиков:

NaN === NaN  // false
NaN !== NaN  // true

IEEE 754 требует этого. Логика такова: если две операции обе провалились, нельзя предполагать, что их “провалы” эквивалентны. Math.sqrt(-1) и 0 / 0 оба дают NaN, но они представляют разные неопределённые результаты.

Это означает, что вы не можете использовать операторы равенства для обнаружения NaN.

Number.isNaN против isNaN: критическая разница

JavaScript предоставляет две функции для обнаружения NaN, но только одна работает надёжно.

Глобальная функция isNaN() сначала приводит свой аргумент к числу:

isNaN("hello")     // true (приводит к NaN, затем проверяет)
isNaN(undefined)   // true
isNaN({})          // true

Это даёт ложные срабатывания. Строка "hello" — это не NaN, это строка, которая становится NaN при приведении типа.

Number.isNaN() проверяет без приведения типа:

Number.isNaN("hello")     // false
Number.isNaN(undefined)   // false
Number.isNaN(NaN)         // true

Всегда используйте Number.isNaN() для точного обнаружения. Альтернативно, Object.is(value, NaN) также работает корректно:

Object.is(NaN, NaN)  // true

Поведение NaN в JSON: молчаливая потеря данных

Когда вы сериализуете данные, содержащие NaN, JSON.stringify() заменяет его на null:

JSON.stringify({ value: NaN })  // '{"value":null}'
JSON.stringify([1, NaN, 3])     // '[1,null,3]'

Это распространённый источник багов при отправке числовых данных в API или сохранении их в базах данных. Ваши значения NaN исчезают молча, и вы получаете обратно null — что может вызвать другие ошибки в дальнейшем.

Валидируйте числовые данные перед сериализацией, если сохранение NaN имеет значение.

structuredClone и NaN: сохранение при современном клонировании

В отличие от JSON, structuredClone() сохраняет значения NaN:

const original = { score: NaN }
const cloned = structuredClone(original)

Number.isNaN(cloned.score)  // true

Это делает structuredClone() безопасным для NaN при глубоком копировании объектов, которые могут содержать некорректные числовые результаты. Если вы клонируете структуры данных с потенциальными значениями NaN, предпочитайте structuredClone() вместо преобразования через JSON.

Распространение NaN: вирусный эффект

Как только NaN попадает в вычисление, он заражает каждый результат:

const result = 5 + NaN      // NaN
const final = result * 100  // NaN

Это распространение сделано намеренно — оно предотвращает случайное использование повреждённых данных. Но это означает, что единственный NaN в начале конвейера может сделать недействительным весь набор данных.

Валидируйте входные данные на границах: при парсинге пользовательского ввода, получении ответов от API или чтении из внешних источников.

Заключение

Странное поведение NaN следует логичным правилам, как только вы понимаете дизайн IEEE 754. Используйте Number.isNaN() или Object.is() для обнаружения — никогда не используйте операторы равенства или глобальную isNaN(). Помните, что JSON-сериализация преобразует NaN в null, в то время как structuredClone() сохраняет его. Валидируйте числовые данные в точках входа, чтобы поймать NaN до того, как он распространится через ваши вычисления.

Часто задаваемые вопросы

NaN определён как специальное числовое значение стандартом IEEE 754 для чисел с плавающей точкой, который реализует JavaScript. Он представляет результат неопределённых математических операций, оставаясь при этом внутри системы числовых типов. Такой дизайн позволяет числовым вычислениям продолжаться без выбрасывания исключений, когда операции проваливаются.

Нет. NaN — это единственное значение в JavaScript, которое не равно самому себе. И NaN === NaN, и NaN == NaN возвращают false. Используйте вместо этого Number.isNaN() или Object.is(value, NaN) для надёжного обнаружения.

Глобальная функция isNaN() приводит свой аргумент к числу перед проверкой, что вызывает ложные срабатывания для нечисловых значений, таких как строки или undefined. Number.isNaN() не выполняет приведение типа и возвращает true только если значение в точности является NaN. Всегда предпочитайте Number.isNaN() для точных результатов.

Валидируйте все числовые входные данные в точках входа, таких как поля форм, ответы API и внешние источники данных. Проверяйте значения с помощью Number.isNaN() сразу после парсинга или получения данных. Это остановит распространение NaN через последующие операции и предотвратит аннулирование ваших результатов.

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Check our GitHub repo and join the thousands of developers in our community.

OpenReplay