Menu

Number и BigInt в JavaScript: точность и большие числа

Разбираемся, как на самом деле устроен тип Number в JavaScript: почему 0.1 + 0.2 ≠ 0.3, что такое MAX_SAFE_INTEGER и когда пора переходить на BigInt.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Один числовой тип почти на все случаи жизни

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

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ