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.

index.js
Output
Click Run to see the output here.

Удобно, правда? Не нужно приводить int к float и обратно, и переполнение на 2^31 тебя не поджидает. Но у представления в виде числа с плавающей точкой есть свои последствия — и новички регулярно на них наступают. В 2020 году в язык добавили второй числовой тип — BigInt, чтобы закрыть те случаи, где обычного Number уже не хватает.

Коварство чисел с плавающей точкой

Запусти вот это:

index.js
Output
Click Run to see the output here.

Первая строка выведет 0.30000000000000004. Вторая — false. И это вовсе не причуда JavaScript: Python, Java, C и любой другой язык, использующий IEEE 754, ведёт себя точно так же.

Причина проста: 0.1 и 0.2 невозможно представить в двоичной системе точно — ровно как 1/3 нельзя записать конечной десятичной дробью. В памяти хранится ближайшее двоичное приближение, а крошечные погрешности накапливаются. Правильная ментальная модель такая: значения Number с дробной частью — это приближения, очень близкие к тому, что вы написали, но не равные ему.

Для денег никогда не храните $19.99 как 19.99. Храните копейки (или центы) целым числом — 1999 — и форматируйте уже при выводе. Это самая полезная привычка, чтобы не нарываться на баги с плавающей точкой.

Как безопасно сравнивать числа с плавающей точкой

Раз уж проверка на равенство ненадёжна, при необходимости сравнивайте с допуском:

index.js
Output
Click Run to see the output here.

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

Диапазон безопасных целых чисел

Целые числа до определённого размера представлены в 64-битном float точно. Но стоит выйти за эту границу — и точность начинает теряться бит за битом:

index.js
Output
Click Run to see the output here.

2^53 - 1 — это последнее целое число, до которого каждое целое представимо точно. Дальше часть целых чисел просто не существует в типе Number — они округляются до ближайшего соседа. И это тихая порча данных, которая рано или поздно выстрелит, если вы парсите 64-битные ID из базы как обычные JSON-числа.

Знакомьтесь, BigInt

BigInt — это отдельный примитив для целых чисел произвольной точности. Создать его можно, добавив n к целочисленному литералу, либо вызвав BigInt(...):

index.js
Output
Click Run to see the output here.

У BigInt нет верхнего предела — ограничивает только доступная память. Это подходящий инструмент для:

  • ID в базе данных или snowflake-идентификаторов Twitter/X, которые выходят за пределы 2^53.
  • Криптографических вычислений.
  • Любой целочисленной арифметики, где точность важнее скорости.

А вот для повседневных счётчиков, индексов массивов или хранения денег в копейках BigInt не подходит — обычный Number работает быстрее и дружит со всеми API языка.

Арифметика с BigInt

Все привычные операторы работают, если оба операнда — BigInt:

index.js
Output
Click Run to see the output here.

Деление округляется к нулю — дробных BigInt не существует. Нужна дробная часть — возвращайтесь к Number (или берите библиотеку для десятичных вычислений).

Не смешивайте типы

Главный подводный камень: в одном выражении нельзя смешивать Number и BigInt.

index.js
Output
Click Run to see the output here.

Сравнение — это единственное исключение: операторы <, >, == умеют приводить типы через границу Number/BigInt:

index.js
Output
Click Run to see the output here.

Получается, == считает их равными, а === — нет. Если вы уже всюду пишете === (а так и надо), то сравнение чисел разных типов — это повод задуматься о дизайне кода. Выберите один тип и приведите к нему.

Преобразование Number в BigInt и обратно

Два направления преобразования — и два подводных камня:

index.js
Output
Click Run to see the output here.

Перевод Number → BigInt работает строго: дробные значения и NaN бросают ошибку. Обратное преобразование BigInt → Number — наоборот, всё «проглотит», но с потерями: всё, что больше MAX_SAFE_INTEGER, округлится. Если BigInt пришёл к вам с сервера и вы собираетесь превратить его в Number, сначала задайте себе вопрос — а точно ли это нужно?

Специальные значения типа Number

Раз уж зашла речь, в типе Number есть три значения, которые с математической точки зрения числами вообще не являются:

index.js
Output
Click Run to see the output here.

Infinity и -Infinity появляются при делении на ноль или когда результат вылезает за пределы диапазона float. А NaN (от "not a number") — это то, что получается, когда арифметическая операция не даёт осмысленного результата.

Знаменитый факт: NaN не равен сам себе — и это не баг JS, а часть спецификации IEEE 754. Для проверки используйте Number.isNaN(x). Старая глобальная функция isNaN сначала приводит аргумент к числу, из-за чего выдаёт неверные ответы (например, isNaN("hello") вернёт true). Всегда отдавайте предпочтение Number.isNaN.

Как преобразовать строку в число в JavaScript

Пользовательский ввод и числа из JSON часто приходят в виде строк. Есть три способа их преобразовать:

index.js
Output
Click Run to see the output here.

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

НАЧАТЬ