Персональный сайт Олега Барабанова

Проблемы больших чисел в JavaScript или зачем нужен тип BigInt

Отметил для себя следующее наблюдение, что многие разработчики на JavaScript невнимательно относятся к проблемам больших чисел в JavaScript, почему-то ожидая, что можно спокойно работать с числами в диапазоне от Number.MIN_VALUE до Number.MAX_VALUE. Типа "64 бита хватит всем" или интерпретатор/компилятор умнее вас и незачем заморачиваться.

Давайте взглянем на достаточно обычную ситуацию. Возьмем например целое число let x = 9007199254740991. Число немаленькое, но спокойно входит в 64 битный диапазон и соответственно нет необходимости прибегать к библиотекам работы с длинной арифметикой, где числа представлены в строковом виде.

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

9007199254740991 + 1 = 9007199254740992
9007199254740991 + 2 = 9007199254740993
9007199254740991 + 3 = 9007199254740994

Вроде бы все правильно и в JavaScript мы не должны ожидать подвоха. Но посмотрим на пример, который я набросал на Codepen.

На примере мы видим что 9007199254740991 + 2 = 9007199254740992. Естественно это не то, что мы ожидали увидеть. Так в чем же проблема?

Особенности хранения числовых значений в JavaScript

Согласно ECMAScript, в JavaScript числа представлены 64-битным числом двойной точности (double precision floating point numbers), описание которых представлено в стандарте IEEE 754.

Рассмотрим проблему на примере числа 9007199254740993. В бинарном виде число 9007199254740993 будет выглядеть как: "100000000000000000000000000000000000000000000000000001"

Начнем с того, что структуру binary64 можно представить в виде 1s11e52m, где:

В итоге, в виде binary64 данное число будет выглядеть как "0_10000110100_0000000000000000000000000000000000000000000000000000" , где:

Но это я описал в очень грубом и наверное не самом лучшем виде. Но в данной статье и не ставилась цель детального разбора проблем представления форматов согласно стандарту IEEE 754. Важно знать, что в JS безопасным является диапазон между -(253 - 1) до 253 - 1.

Проблема больших чисел в JS при применении метода JSON.parse() ?

Где можно неожиданно столкнуться с проблемой больших чисел, так это при разборе JSON.

Например

JSON.parse("[9007199254740991, 9007199254740992, 9007199254740993]");
// ↪ [9007199254740991,9007199254740992,9007199254740992]
JSON.parse("23845061135960375"); // ↪ 23845061135960376

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

Применение типа BigInt для большим чисел в JavaScript.

Специально для ситуации с работой больших чисел, в ECMAScript 2020 введен новый тип BigInt для сохранения точности числа при работе с ним. К сожалению, на август 2020 поддерживают данную функциональность ~75% браузеров, согласно caniuse.com .

Тип BigInt в JS можно задать двумя способами:

let var1 = BigInt("9007199254740992");
let var2 = 9007199254740991n; //добавляем символ "n" в конце.

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

BigInt("9007199254740991") + 2;
// ↪ Wrong => Uncaught TypeError: Cannot mix BigInt and other types,...
BigInt("9007199254740991") + BigInt(2);
// ↪ Right => 9007199254740993n

Кстати, для упрощения, в современных версиях JS существуют константные значения Number.MIN_SAFE_INTEGER и Number.MAX_SAFE_INTEGER, равные минимальному и максимальному значению, которое можно безопасно представлять типом Number. Тем не менее, рекомендую для возможных значений, где вероятны большие числовые значения, сразу представлять значения в виде BigInt, а в запросах передавать такие числа в виде строкового значения, т.к. это позволит избежать существенных проблем в будущем. Как пример:

let x = JSON.parse('{"id":"23845061135960375"}');
BigInt(x.id);
// ↪ Right => 23845061135960375n

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