BigInt: целочисленные значения произвольной точности в JavaScript
BigInt
s — это новый числовой примитив в JavaScript, который может представлять целые числа произвольной точности. С помощью BigInt
s можно безопасно хранить и оперировать большими целыми числами даже за пределами допустимой границы для Number
. В этой статье рассматриваются возможные случаи использования и объясняется новая функциональность в Chrome 67 путем сравнения BigInt
s с Number
s в JavaScript.
Случаи использования
Целые числа произвольной точности открывают множество новых возможностей для JavaScript.
BigInt
s позволяют выполнять целочисленные арифметические операции без переполнения. Это само по себе открывает бесчисленное множество новых возможностей. Например, математические операции над большими числами часто используются в финансовых технологиях.
Большие целочисленные идентификаторы и временные метки высокой точности не могут быть безопасно представлены как Number
s в JavaScript. Это часто приводит к ошибкам в реальном мире и заставляет разработчиков JavaScript представлять их как строки. С помощью BigInt
эти данные теперь могут быть представлены как числовые значения.
BigInt
может стать основой для будущей реализации BigDecimal
. Это было бы полезно для представления денежных сумм с десятичной точностью и точного выполнения операций с ними (проблема 0.10 + 0.20 !== 0.30
).
Ранее приложения JavaScript с любым из этих случаев использования вынуждены были прибегать к пользовательским библиотекам, которые эмулируют функциональность, похожую на BigInt
. Когда BigInt
станет широко доступен, такие приложения смогут отказаться от этих зависимостей времени выполнения в пользу встроенных BigInt
s. Это помогает снизить время загрузки, время разбора и время компиляции, а также улучшает производительность времени выполнения.
Текущее состояние: Number
Number
s в 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
BigInt
s — это новый числовой примитив в JavaScript, который может представлять целые числа с произвольной точностью. С BigInt
s можно безопасно хранить и оперировать большими целыми числами даже за пределами безопасной границы для 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
). Однако результат заканчивается на кучу нулей. Это неправильно! Давайте попробуем снова с использованием BigInt
s:
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 с деталями реализации.