Один числовой тип почти на все случаи жизни
В большинстве языков целые числа и числа с плавающей точкой — это разные типы. В JavaScript исторически всё держалось на одном — Number. Пишете вы 42, 3.14 или -0.001 — под капотом один и тот же примитив: 64-битное число с плавающей точкой двойной точности по стандарту IEEE 754.
Удобно, правда? Не нужно приводить int к float и обратно, и переполнение на 2^31 тебя не поджидает. Но у представления в виде числа с плавающей точкой есть свои последствия — и новички регулярно на них наступают. В 2020 году в язык добавили второй числовой тип — BigInt, чтобы закрыть те случаи, где обычного Number уже не хватает.
Коварство чисел с плавающей точкой
Запусти вот это:
Первая строка выведет 0.30000000000000004. Вторая — false. И это вовсе не причуда JavaScript: Python, Java, C и любой другой язык, использующий IEEE 754, ведёт себя точно так же.
Причина проста: 0.1 и 0.2 невозможно представить в двоичной системе точно — ровно как 1/3 нельзя записать конечной десятичной дробью. В памяти хранится ближайшее двоичное приближение, а крошечные погрешности накапливаются. Правильная ментальная модель такая: значения Number с дробной частью — это приближения, очень близкие к тому, что вы написали, но не равные ему.
Для денег никогда не храните $19.99 как 19.99. Храните копейки (или центы) целым числом — 1999 — и форматируйте уже при выводе. Это самая полезная привычка, чтобы не нарываться на баги с плавающей точкой.
Как безопасно сравнивать числа с плавающей точкой
Раз уж проверка на равенство ненадёжна, при необходимости сравнивайте с допуском:
Number.EPSILON — это минимальная разница между 1 и следующим представимым числом, то есть разумная погрешность по умолчанию для значений в районе единицы. Для очень больших или очень маленьких величин стоит брать допуск, который масштабируется вместе со входными данными.
Диапазон безопасных целых чисел
Целые числа до определённого размера представлены в 64-битном float точно. Но стоит выйти за эту границу — и точность начинает теряться бит за битом:
2^53 - 1 — это последнее целое число, до которого каждое целое представимо точно. Дальше часть целых чисел просто не существует в типе Number — они округляются до ближайшего соседа. И это тихая порча данных, которая рано или поздно выстрелит, если вы парсите 64-битные ID из базы как обычные JSON-числа.
Знакомьтесь, BigInt
BigInt — это отдельный примитив для целых чисел произвольной точности. Создать его можно, добавив n к целочисленному литералу, либо вызвав BigInt(...):
У BigInt нет верхнего предела — ограничивает только доступная память. Это подходящий инструмент для:
- ID в базе данных или snowflake-идентификаторов Twitter/X, которые выходят за пределы
2^53. - Криптографических вычислений.
- Любой целочисленной арифметики, где точность важнее скорости.
А вот для повседневных счётчиков, индексов массивов или хранения денег в копейках BigInt не подходит — обычный Number работает быстрее и дружит со всеми API языка.
Арифметика с BigInt
Все привычные операторы работают, если оба операнда — BigInt:
Деление округляется к нулю — дробных BigInt не существует. Нужна дробная часть — возвращайтесь к Number (или берите библиотеку для десятичных вычислений).
Не смешивайте типы
Главный подводный камень: в одном выражении нельзя смешивать Number и BigInt.
Сравнение — это единственное исключение: операторы <, >, == умеют приводить типы через границу Number/BigInt:
Получается, == считает их равными, а === — нет. Если вы уже всюду пишете === (а так и надо), то сравнение чисел разных типов — это повод задуматься о дизайне кода. Выберите один тип и приведите к нему.
Преобразование Number в BigInt и обратно
Два направления преобразования — и два подводных камня:
Перевод Number → BigInt работает строго: дробные значения и NaN бросают ошибку. Обратное преобразование BigInt → Number — наоборот, всё «проглотит», но с потерями: всё, что больше MAX_SAFE_INTEGER, округлится. Если BigInt пришёл к вам с сервера и вы собираетесь превратить его в Number, сначала задайте себе вопрос — а точно ли это нужно?
Специальные значения типа Number
Раз уж зашла речь, в типе Number есть три значения, которые с математической точки зрения числами вообще не являются:
Infinity и -Infinity появляются при делении на ноль или когда результат вылезает за пределы диапазона float. А NaN (от "not a number") — это то, что получается, когда арифметическая операция не даёт осмысленного результата.
Знаменитый факт: NaN не равен сам себе — и это не баг JS, а часть спецификации IEEE 754. Для проверки используйте Number.isNaN(x). Старая глобальная функция isNaN сначала приводит аргумент к числу, из-за чего выдаёт неверные ответы (например, isNaN("hello") вернёт true). Всегда отдавайте предпочтение Number.isNaN.
Как преобразовать строку в число в JavaScript
Пользовательский ввод и числа из JSON часто приходят в виде строк. Есть три способа их преобразовать:
Number() работает строго: всё, что не похоже на число, превращается в NaN. Исключение — пустая строка и пробелы, они дают 0. А вот parseInt и parseFloat ведут себя снисходительно — читают строку, пока получается, и останавливаются. Выбирайте то, что соответствует вашей задаче, и обязательно проверяйте результат на NaN, прежде чем его использовать.
Чтобы распарсить BigInt из строки, используйте BigInt("123") — этот способ строгий и бросает исключение, если на входе мусор.
Краткая шпаргалка
- Для счётчиков, математики, координат и большинства повседневных чисел —
Number. - Для денег — переводите в целые копейки и работайте через
Number, либо подключайте библиотеку для десятичной арифметики. - Для целых чисел больше
2^53(идентификаторы в БД, криптография, комбинаторика) —BigIntс суффиксомn. - Числа с плавающей точкой сравнивайте с допуском, а не через
===. - Для проверки «битых» результатов используйте
Number.isNaNиNumber.isFinite, а не глобальные функции. - Не смешивайте
NumberиBigIntв одном выражении — приводите типы явно.
Дальше: null vs undefined
В JavaScript есть два способа сказать «значения нет» — null и undefined — и они не взаимозаменяемы. В следующей главе разберём, что означает каждое из них, чем они отличаются и когда какое использовать.
Часто задаваемые вопросы
Почему 0.1 + 0.2 в JavaScript не равно 0.3?
Потому что тип Number в JavaScript — это 64-битное число с плавающей точкой по стандарту IEEE 754, и числа 0.1 и 0.2 в двоичном виде точно не представляются. В результате получаем 0.30000000000000004. Это не баг JavaScript — ровно то же самое будет в Python, Java и в любом другом языке с таким же форматом float. Если считаете деньги — храните всё в копейках (то есть в целых числах) или подключайте библиотеку для работы с десятичными дробями.
Что такое BigInt и когда его стоит использовать?
BigInt — это отдельный числовой примитив для целых чисел, которые выходят за пределы Number.MAX_SAFE_INTEGER (2^53 − 1). Создать можно литералом с суффиксом n — например, 9007199254740993n — или через BigInt(value). Пригодится для 64-битных ID из базы, криптографии и любых задач, где точность целочисленной арифметики важнее скорости.
Можно ли смешивать Number и BigInt в одном выражении?
Нельзя. Выражение 1n + 1 выбросит TypeError: Cannot mix BigInt and other types. Приводите типы явно: BigInt(n) или Number(b). При этом операторы сравнения вроде < и == между ними работают, а вот === всегда вернёт false, потому что типы разные.