Один числовой тип почти на все случаи жизни
В большинстве языков целые числа и числа с плавающей точкой - это разные типы. В 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, потому что типы разные.