BigInt — длинная арифметика в JavaScript

Автор оригинала: Mathias Bynens
  • Перевод

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


Примеры использования


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


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


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


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


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


https://habrastorage.org/webt/ep/l8/hc/epl8hchee6j7hskpzq0g_ivp1he.png
Нативный BigInt работает быстрее, чем популярные пользовательские библиотеки


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


Статус-кво: Number


Примитивный тип данных Number в 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 не сможет точно сохранить результат:


max + 2;
// → 9_007_199_254_740_992

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


Новинка: BigInt


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


Для создания BigInt достаточно добавить суффикс n к литеральной записи целого числа. Например, 123 станет 123n. Глобальную функцию BigInt(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:


1234567890123456789n * 123n;
// → 151851850485185185047n 

В этот раз результат верный.


Пределы для безопасной работы с целыми числами не применимы к 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', because `0n` is falsy.

Операторы


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


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


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

Примечание: поскольку BigInt и Number обычно не смешиваются, не стоит переписывать уже существующий код с Number на BigInt. Решите, какой из этих двух типов вам нужен, и используйте его. Для новых 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 указанным числом значащих бит, рассматривая при этом число либо как знаковое, либо как беззнаковое. BigInt.asIntN(width, value) ограничит число value типа BigInt указанным в width числом бит с учётом знака, а BigInt.asUintN(width, value) сделает то же самое, рассматривая значение как беззнаковое. Например, если вам необходимы операции на 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!

Поддержать автора
Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 29

  • НЛО прилетело и опубликовало эту надпись здесь
      0

      О каком непочиненном костыле речь?

        +2

        Наверно речь о том что в JS не хватает целых чисел ((u)int32, u(int64)), и использовать вместо них float это костыль, в тех случаях когда предметная область предполагает целые числа.


        Теперь же, вместо того чтобы добавить целые числа определённого размера, добавили BigNumber — что намного менее производительно и во многих случаях могло бы быть заменено на int64 или int128.

          +1
          Почему не int256 или int512?
            +1
            int32 не имеет смысла, т. к. движок (по крайней мере V8) итак использует SMI, который аналогичен int32 (на 64 бит) или int31 (на 32 бит). Double используется только в случаях, когда вы выходите за указанный диапазон.

            А вот отсутствие (u)int64 да, обидно.
          –4
          Причём тут костыль? Вы со стандартом IEEE 754 не знакомы? Так познакомьтесь, а потом умничайте.
            0

            Вы n пропустили в записи числа.

            +1
            Жаль только реально использовать можно будет года через два.
            А так хорошее нововведение, вот как раз недавно страдал, что в 2^53 идентификаторы не влезают (а в 2^64 влезли бы).
              +1
              Я однажды чуть не тронулся, когда онлайн-просмотрщик JSON начал округлять 64-битные айдишники, и запросы в БД руками с неявно округленным айди и из скрипта с правильным давали разные результаты
              +6
              Унарный — можно использовать для обозначения отрицательного значения BigInt, например, -42n. Унарный + не поддерживается, потому что он нарушит код asm.js, который ожидает, что +x всегда будет возвращать либо Number, либо исключение.

              Ну вот, добавили полезную фичу, чтоб пофиксить костыль, но и она вышла с костылем.
                +5

                А что насчёт передачи bigint в json?
                Как нужно будет парсить строку и понимать, что там находится?
                Ведь нельзя складывать(умножать, ..) bigint с обычными числами

                  +2
                  Комитет до сих пор не определился, текущая реализация подразумевает кидать ошибку при сериализации.

                  JSON.stringify({a:123n})
                  
                  VM97:1 Uncaught TypeError: Do not know how to serialize a BigInt
                      at JSON.stringify (<anonymous>)
                  
                    0
                    Видимо, пока нужно знать, где чего находиться, чтобы парсить значение из нужного поля руками, а также воткнуть костыль для перевода BigInt в строку при сериализации вместо исключения. Например, так: jsfiddle.net/c0hhydy4/2
                    –1

                    Почему только BigInt? Почему бы заодно не сделать BigFloat или BigNumber?

                      0
                      Про decimal уже написали же
                        0
                        Уточните, пожалуйста, а где именно писали? Не могу найти.
                      –5
                      математические операции над большими числами обычно используются в финансовых технологиях

                      Чтобы подсчитать, сколько нашли денег у главы питерского ростехнадзора. )
                      Простите, не удержался.

                        0
                        Буду читать комментарии перед тем как задать вопрос )
                          +1
                          Не понимаю, откуда в JavaScript настолько острая потребность в длинных числах, что нельзя обойтись библиотекой.
                            0

                            Лично я уже пробовал использовать в качестве ключей для Map.


                            Для пространства 2D плоскость 32bitx32bit = 64bit ключ для Map, что бы не ремапить проекцию. (Вечно перепиливаемая заново браузерная игрушка-платформер с "бесконечным" миром)


                            Сейчас для ключа с быстрыми побитовыми операциями (работа с тайлами), безопасно использовать только по 16bit (x + y<<16, с number побитовая арифметика корректна только в приделах 32бит ключ), т.е. 64536 x ..., а с bigint 32bit x ...

                              0
                              Плюсуюсь к комментарию выше — они нужны для ключей Set/Map.
                              Там годятся только элементарные типы — числа или строки, но не объекты.
                              Точнее, чисто технически объекты использовать тоже можно, но смысла в этом нет, потому что для равенства ключей не годится объект-копия с аналогичными значениями полей, нужен только строго именно тот же самый объект.
                                0
                                Решается хранением длинных чисел в виде строк. Это тянет на острую необходимость, только если у значительного числа разработчиков возникает такая потребность. Это разве так?
                                  0
                                  Решается, но ценой серьезной просадки перфоманса, проверено.
                                  Насчет острая или не острая — вопрос философский. Не нужно — не используйте, в чем проблема?
                              +1

                              Теперь браузер можно повесить еще проще...


                              1n << 10000000n


                              P.S. chrome unstable — 68.0.3418.2 (Official Build) dev (64-bit)

                                +1
                                А почему asm.js регулирует эволюцию языка?
                                  0
                                  Как возможно использовать BigInt? В браузере не наблюдаю эту возможность
                                    0
                                    В первом абзаце упоминается Chrome 67
                                    0
                                    эм, скомпилил ноду с последним V8 и тут это:
                                    ./node
                                    > 123n*1
                                    TypeError: Cannot mix BigInt and other types, use explicit conversions

                                    так и задумывалось?
                                      +1
                                      Верно, так и задумывалось. В переводе об этом есть.

                                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                    Самое читаемое