Валерий Шестернин
В данной статье мы рассмотрим числовые типы данных в JavaScript, разберем откуда появляются неточности в вычислениях, а также познакомимся с различными способами записи числовых значений и узнаем как применять все это на практике в своих проектах.
Number и BigInt
С момента своего создания в 1995 году, в JavaScript был только один числовой тип данных - Number, который имеет ограничения в пределах 64-битового формата IEEE-754. Это означает, что безопасно доступны целые числа в диапазоне от-(2^53 - 1) до (2^53 - 1) из-за ограничений на хранение позиции точки. Поэтому числа, выходящие за эти границы, будут преобразованы в Infinity или -Infinity. Однако с выходом ES2020 появился новый тип данных - BigInt, который позволяет работать с целыми числами любой длинны. Для создания значения с таким типом достаточно добавить n в конце числа при вводе вручную или передать нужное значение в функцию BigInt.
console.log(10 ** 1000); //Infinity
console.log(10n ** 1000n); //выведет все значение огромным числом
BigInt во многих ситуациях ведет себя как Number, например с большинством математических операторов, а так же при преобразовании в Boolean
, использовании логических операторов ||
, &&
, !
и при проверке условий. Но не может его полностью заменить т.к. имеет ряд ограничений:
- Оператор деления округляет результат в меньшую сторону до целого потому что BigInt представляет только целые числа.
- BigInt нельзя смешивать в одних выражениях с Number, для подобных операций следует приводить значения к одному типу данных, но не стоит забывать, что при приведении Number к BigInt потеряется вся дробная часть, а при приведении BigInt к Number - точность и обход ограничения максимального значения.
- Унарный плюс не подойдет для приведения BigInt к Number поэтому следует использовать глобальный объект
Number
. - Строгое сравнение “одинаковых” значений этих двух типов данных вернет false (
1n === 1 //false
), при этом другие операторы сравнения будут работать как обычно вне зависимости от того, какими типами данных представлены числа, как и метод.sort
массивов, элементы которых являются любым из числовых типов. - Для использования объекта Math BigInt так же придется приводить к Number.
- Если вы попытаетесь использовать функцию
JSON.stringify()
на значении типа BigInt, то столкнетесь с ошибкой TypeError. Это происходит потому что по умолчанию значения BigInt не могут быть правильно преобразованы в формат JSON.
console.log(3n / 2n); //1n
//результат округлен с 1,5 до целого 1
console.log(3n / 2); //TypeError: Cannot mix BigInt and other types
console.log(BigInt(1.5));
//RangeError: The number 1.5 cannot be converted to a BigInt because it is not an integer
console.log(BigInt(Math.round(1.5))); //2n
//для преобразования Number в BigInt придется округлять значение
console.log(+2n); //TypeError: Cannot convert a BigInt value to a number
console.log(Number(2n)); //2
//Для конвертации BigInt в Number нужно использовать глобальный объект Number
console.log(JSON.stringify(2n)); //TypeError: Do not know how to serialize a BigInt
Неточности при вычислениях в JavaScript
BigInt призван не только снять ограничение по размеру положительных и отрицательных числе в JavaScript, но и решить проблему неточности при работе с большими числами. Как мы уже знаем, JavaScript хранит Number в 64-битном формате IEEE-754. Из этих 64 бит один хранит знак числа, 11 - позицию десятичной дроби и оставшиеся 52 - само число и если число занимает больше 52 бит - часть информации будет утрачена. BigInt предотвращает такую потерю.
console.log(9999999999999999);
//выведет 10000000000000000
console.log(9999999999999999n);
//выведет 9999999999999999n
Однако BigInt подходит только для целых чисел. При работе дробями так же могут возникать неточности вычислений из-за переполнения памяти, отведенной под знаки после запятой. На самом деле такие неточности возникают чаще, чем кажется, но формат IEEE-754 незаметно для нас округляет дробь до ближайшего возможного числа. Видим же мы последствия переполнения 11 бит, в основном при математических операциях с дробями. Тут уже нам самим нужно при написании кода позаботиться о корректности вывода результатов операций с дробями, например округлив этот результат до допустимого знака после запятой с помощью метода .toFixed()
.
console.log((0.1).toFixed(18)); //0.100000000000000006
//можно увидеть 'отрезаную' стандартом неточность дроби
const sum = 0.1 + 0.2;
console.log(sum); //0.30000000000000004
//христоматийный пример неточности вычислений с плавающей запятой
console.log(sum.toFixed(1));//0.3
//а так легко его можно исправить, если нам важен только один знак после запятой
Способы записи чисел в JavaScript
В JavaScript существует несколько способов записать число с большим количеством знаков так, что бы его было удобно прочесть. Так знак нижнего подчеркивания ( _ ) внутри записи числа не повлияет на его значение, но упростит прочтение, а для очень больших или очень маленьких чисел, с большим количеством знаков знаков после запятой, можно добавить букву “e” и указать после нее количество нулей (положительное для записи целого числа и отрицательное для дроби). Так миллиард можно записать как 1e9, а одну миллиардную долю как 1e-9. Важно помнить что запись через “e” не доступна в BigInt и не защищает от неточности вычислений.
console.log(1_000_000); //1000000
console.log(1_000_000n); //1000000n
console.log(1e23n);//SyntaxError: Invalid or unexpected token
console.log(0.00000000000000000000001);//1e-23
const myNum = 1e23;
const myBigInt = 99999999999999991611392n;
console.log(myBigInt == myNum); //true
Довольно часто встречаются записи значений в виде строки с единицей измерения, например 100px или 20$. Что бы извлечь из такой записи числовое значение можно воспользоваться функциями parseInt()
для целых чисел или parseFloat()
для дробей. Однако стоит помнить, что обе эти функции “ищут” числовые значения с начала строки и вернут NaN при парсинге строки типа “у Саши было 2 яблока”, проигнорировав цифру 2. Для поиска по подобным строкам лучше использовать регулярные выражения.
console.log(parseInt("100px")); //100
console.log(parseFloat("192.168.0.1")); //192.168
//поиск останавливается после первого нечислового знака
console.log(parseInt("I am 23 years old")); //NaN
console.log("I am 23 years old".match(/-?\d+(\.\d+)?/g)); //[ '23' ]
Заключение
JavaScript, как и большинство языков программирования, имеет неточности при работе с числами поэтому понимание причин таких неточностей и знание способов обхода ограничений при работе с числовыми типами данных крайне важно для профессионального роста разработчика. Надеюсь что данная статья была полезна для понимания столь важной темы.
Комментарии
0