Перейти к основному содержимому

BigInt: целочисленные значения произвольной точности в JavaScript

· 9 мин. чтения
Mathias Bynens ([@mathias](https://twitter.com/mathias))

BigInts — это новый числовой примитив в JavaScript, который может представлять целые числа произвольной точности. С помощью BigInts можно безопасно хранить и оперировать большими целыми числами даже за пределами допустимой границы для Number. В этой статье рассматриваются возможные случаи использования и объясняется новая функциональность в Chrome 67 путем сравнения BigInts с Numbers в JavaScript.

Случаи использования

Целые числа произвольной точности открывают множество новых возможностей для JavaScript.

BigInts позволяют выполнять целочисленные арифметические операции без переполнения. Это само по себе открывает бесчисленное множество новых возможностей. Например, математические операции над большими числами часто используются в финансовых технологиях.

Большие целочисленные идентификаторы и временные метки высокой точности не могут быть безопасно представлены как Numbers в JavaScript. Это часто приводит к ошибкам в реальном мире и заставляет разработчиков JavaScript представлять их как строки. С помощью BigInt эти данные теперь могут быть представлены как числовые значения.

BigInt может стать основой для будущей реализации BigDecimal. Это было бы полезно для представления денежных сумм с десятичной точностью и точного выполнения операций с ними (проблема 0.10 + 0.20 !== 0.30).

Ранее приложения JavaScript с любым из этих случаев использования вынуждены были прибегать к пользовательским библиотекам, которые эмулируют функциональность, похожую на BigInt. Когда BigInt станет широко доступен, такие приложения смогут отказаться от этих зависимостей времени выполнения в пользу встроенных BigInts. Это помогает снизить время загрузки, время разбора и время компиляции, а также улучшает производительность времени выполнения.

Встроенная реализация BigInt в Chrome работает лучше, чем популярные пользовательские библиотеки.

Текущее состояние: Number

Numbers в JavaScript представлены как числа с двойной точностью. Это означает, что их точность ограничена. Константа Number.MAX_SAFE_INTEGER предоставляет наибольшее возможное целое число, которое можно безопасно инкрементировать. Его значение равно 2**53-1.

const max = Number.MAX_SAFE_INTEGER;
// → 9_007_199_254_740_991
примечание

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

Инкрементирование его один раз даёт ожидаемый результат:

max + 1;
// → 9_007_199_254_740_992 ✅

Но если мы инкрементируем его второй раз, результат больше не может быть точно представлен как Number в JavaScript:

max + 2;
// → 9_007_199_254_740_992 ❌

Обратите внимание, как max + 1 дает тот же результат, что и max + 2. Всякий раз, когда мы получаем это конкретное значение в JavaScript, нет возможности определить, является ли оно точным. Любая операция с целыми числами за пределами диапазона безопасных целых чисел (то есть от Number.MIN_SAFE_INTEGER до Number.MAX_SAFE_INTEGER) потенциально теряет точность. По этой причине мы можем полагаться только на числовые целые значения в пределах безопасного диапазона.

Новая горячая новинка: BigInt

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

Чтобы создать BigInt, добавьте суффикс n к любому целочисленному литералу. Например, 123 становится 123n. Глобальная функция BigInt(number) может быть использована для преобразования Number в BigInt. Другими словами, BigInt(123) === 123n. Давайте используем эти две техники, чтобы решить проблему, с которой мы столкнулись ранее:

BigInt(Number.MAX_SAFE_INTEGER) + 2n;
// → 9_007_199_254_740_993n ✅

Вот ещё один пример, где мы умножаем два Number:

1234567890123456789 * 123;
// → 151851850485185200000 ❌

Смотрим на наименее значимые цифры, 9 и 3, и понимаем, что результат умножения должен заканчиваться на 7 (поскольку 9 * 3 === 27). Однако результат заканчивается на кучу нулей. Это неправильно! Давайте попробуем снова с использованием BigInts:

1234567890123456789n * 123n;
// → 151851850485185185047n ✅

На этот раз мы получаем правильный результат.

Ограничения безопасных целых чисел для Number не применяются к BigInt. Поэтому с помощью BigInt мы можем выполнять точную арифметику, не беспокоясь о потере точности.

Новый примитив

BigInt — это новый примитив в языке JavaScript. Как таковой, он имеет свой собственный тип, который можно обнаружить с помощью оператора typeof:

typeof 123;
// → 'number'
typeof 123n;
// → 'bigint'

Поскольку BigInt — это отдельный тип, BigInt никогда не равен строго Number, например, 42n !== 42. Чтобы сравнить BigInt и Number, преобразуйте один из них в тип другого перед сравнением или используйте абстрактное равенство (==):

42n === BigInt(42);
// → true
42n == 42;
// → true

При принудительном преобразовании в логическое значение (что происходит, например, при использовании if, &&, || или Boolean(int)), BigInt следуют той же логике, что и Number.

if (0n) {
console.log('if');
} else {
console.log('else');
}
// → logs 'else', потому что `0n` является ложным значением.

Операторы

BigInt поддерживает самые распространенные операторы. Бинарные +, -, * и ** работают как ожидается. / и % работают и округляют в сторону нуля по необходимости. Побитовые операции |, &, <<, >> и ^ выполняют арифметику на основе представления в дополнительном коде для отрицательных значений, как и для Number.

(7 + 6 - 5) * 4 ** 3 / 2 % 3;
// → 1
(7n + 6n - 5n) * 4n ** 3n / 2n % 3n;
// → 1n

Унарный - может использоваться для обозначения отрицательного значения BigInt, например, -42n. Унарный + не поддерживается, так как это нарушило бы код asm.js, который ожидает, что +x всегда возвращает либо Number, либо исключение.

Одной из особенностей является то, что операции между BigInt и Number смешивать нельзя. Это положительный момент, поскольку любое неявное преобразование могло бы привести к потере информации. Рассмотрим этот пример:

BigInt(Number.MAX_SAFE_INTEGER) + 2.5;
// → ?? 🤔

Каков должен быть результат? Здесь нет хорошего ответа. BigInt не может представлять дроби, а Number не может представлять BigInt за пределами безопасного диапазона целых чисел. По этой причине смешивание операций между BigInt и Number приводит к исключению TypeError.

Единственным исключением из этого правила являются операторы сравнения, такие как === (как обсуждалось ранее), < и >= — потому что они возвращают логическое значение, риска потери точности нет.

1 + 1n;
// → TypeError
123 < 124n;
// → true

Поскольку BigInt и Number в общем случае несовместимы, избегайте перегрузок или магической «апгрейдации» существующего кода для использования BigInt вместо Number. Выберите одну из этих двух сфер работы, а затем придерживайтесь её. Для новых API, работающих с потенциально большими целыми числами, BigInt является лучшим выбором. Number по-прежнему имеет смысл для целых значений, которые находятся в диапазоне безопасных целых чисел.

Еще одно, что следует отметить, это оператор >>>, который выполняет беззнаковый сдвиг вправо, не имеет смысла для BigInt, так как они всегда знаковые. По этой причине >>> не работает для BigInt.

API

Доступны несколько новых API, специфичных для BigInt.

Глобальный конструктор BigInt аналогичен конструктору Number: он преобразует свой аргумент в BigInt (как упоминалось ранее). Если преобразование не удается, выбрасывается исключение SyntaxError или RangeError.

BigInt(123);
// → 123n
BigInt(1.5);
// → RangeError
BigInt('1.5');
// → SyntaxError

В первом из приведенных примеров передается числовой литерал в BigInt(). Это плохая практика, так как Number страдают от потери точности, и мы уже можем потерять точность до того, как произойдет преобразование в BigInt:

BigInt(123456789123456789);
// → 123456789123456784n ❌

По этой причине рекомендуется придерживаться либо нотации литерала BigInt (с суффиксом n), либо передавать строку (а не Number!) в BigInt():

123456789123456789n;
// → 123456789123456789n ✅
BigInt('123456789123456789');
// → 123456789123456789n ✅

Две библиотечные функции позволяют обернуть значения BigInt как знаковые или беззнаковые целые числа, ограниченные определенным числом битов. BigInt.asIntN(width, value) оборачивает значение BigInt в двоичное знаковое целое число шириной width, а BigInt.asUintN(width, value) оборачивает значение BigInt в двоичное беззнаковое целое число шириной width. Если вы выполняете арифметику с 64-битными числами, например, вы можете использовать эти API, чтобы оставаться в заданном диапазоне:

// Максимальное возможное значение BigInt, которое можно
// представить как знаковое 64-битное целое число.
const max = 2n ** (64n - 1n) - 1n;
BigInt.asIntN(64, max);
9223372036854775807n
BigInt.asIntN(64, max + 1n);
// → -9223372036854775808n
// ^ отрицательное из-за переполнения

Обратите внимание, как возникает переполнение, как только мы переходим к значению BigInt, превышающему диапазон 64-битного целого числа (то есть 63 бита для абсолютного числового значения + 1 бит для знака).

BigInt позволяет точно представлять знаковые и беззнаковые 64-битные целые числа, которые часто используются в других языках программирования. Два новых типа массивов, BigInt64Array и BigUint64Array, упрощают эффективное представление и работу со списками таких значений:

const view = new BigInt64Array(4);
// → [0n, 0n, 0n, 0n]
view.length;
// → 4
view[0];
// → 0n
view[0] = 42n;
view[0];
// → 42n

Тип BigInt64Array гарантирует, что его значения остаются в пределах диапазона знакового 64-битного числа.

// Наибольшее возможное значение BigInt,
// которое может быть представлено как знаковое 64-битное целое число.
const max = 2n ** (64n - 1n) - 1n;
view[0] = max;
view[0];
// → 9_223_372_036_854_775_807n
view[0] = max + 1n;
view[0];
// → -9_223_372_036_854_775_808n
// ^ отрицательное из-за переполнения

Тип BigUint64Array делает то же самое, используя диапазон беззнаковых 64-битных чисел.

Полифилинг и транспиляция BigInt

На момент написания BigInt поддерживаются только в Chrome. Остальные браузеры активно работают над их внедрением. Но что, если вы хотите использовать функциональность BigInt уже сегодня без ущерба для совместимости браузеров? Рад, что вы спросили! Ответ… довольно интересный.

В отличие от большинства других современных функций JavaScript, BigInt не может быть разумно транспилирован до ES5.

Предложение BigInt изменяет поведение операторов (например, +, >= и т. д.), чтобы они работали с BigInt. Эти изменения невозможно напрямую полифилить, а также они делают неприемлемым (в большинстве случаев) транспиляцию кода с BigInt в код для обратной совместимости с помощью Babel или подобных инструментов. Причина в том, что такая транспиляция потребует замены каждого оператора в программе вызовом функции, проверяющей типы входных данных, что приведет к неприемлемому штрафу за производительность во время выполнения. Кроме того, это значительно увеличит размер любого транспилированного пакета, что отрицательно скажется на времени загрузки, разбора и компиляции.

Более осуществимым и устойчивым решением является написание вашего кода с использованием библиотеки JSBI на данный момент. JSBI — это порт JavaScript реализации BigInt в V8 и Chrome — по дизайну он ведет себя точно так же, как и нативная функциональность BigInt. Разница в том, что вместо синтаксиса используется API:

import JSBI from './jsbi.mjs';

const max = JSBI.BigInt(Number.MAX_SAFE_INTEGER);
const two = JSBI.BigInt('2');
const result = JSBI.add(max, two);
console.log(result.toString());
// → '9007199254740993'

После того как BigInt будет нативно поддерживаться во всех браузерах, которые вы используете, вы можете использовать babel-plugin-transform-jsbi-to-bigint для транспиляции вашего кода в нативный код BigInt и отказаться от зависимости от JSBI. Например, приведенный выше пример транспилируется в:

const max = BigInt(Number.MAX_SAFE_INTEGER);
const two = 2n;
const result = max + two;
console.log(result);
// → '9007199254740993'

Дополнительное чтение

Если вас интересует, как BigInt работает за кулисами (например, как они представлены в памяти и как выполняются операции), прочитайте наш пост в блоге V8 с деталями реализации.

Поддержка BigInt