Проблемы больших чисел в 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, где:
- 1s = 1 бит, по которому определяется, положительное или отрицательное число.
- 11e = 11 бит, на экспоненту, которая определяет количество сдвигов запятой
- 52m = 52 бит выделяется на мантиссу
В итоге, в виде binary64 данное число будет выглядеть как "0_10000110100_0000000000000000000000000000000000000000000000000000" , где:
- 0 - флаг положительного числа (1 была бы при отрицательном);
- 10000110100 - экспонента, которая считалась как 53 шага до нормализации (т.е. 53 смещения запятой с 100000000000000000000000000000000000000000000000000001.0 до 1.00000000000000000000000000000000000000000000000000001) + 1023 = 1076 = 10000110100 (в двоичном виде).
- 0000000000000000000000000000000000000000000000000000 - мантисса. Заметьте, единичка в конце не влезла в 52 знака и пришлось её округлить/отбросить (детали округления можно изучить в IEEE 754). И именно из-за этого округления, при обратном преобразовании, вы получите результат не 9007199254740993, а именно 9007199254740992 !
Но это я описал в очень грубом и наверное не самом лучшем виде. Но в данной статье и не ставилась цель детального разбора проблем представления форматов согласно стандарту 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 может быть необходимо быть знакомым с особенностями машинной арифметики и базовыми стандартами.