В современном JavaScript существует два типа чисел:
- Обычные числа в JavaScript хранятся в 64-битном формате IEEE-754, который также называют «числа с плавающей точкой двойной точности» (double precision floating point numbers). Это числа, которые мы будем использовать чаще всего. Мы поговорим о них в этой главе.
BigIntчисла дают возможность работать с целыми числами произвольной длины. Они нужны достаточно редко и используются в случаях, когда необходимо работать со значениями более чем(253-1)или менее чем-(253-1). Так какBigIntчисла нужны достаточно редко, мы рассмотрим их в отдельной главе BigInt.
В данной главе мы рассмотрим только первый тип чисел: числа типа number. Давайте глубже изучим, как с ними работать в JavaScript.
Способы записи числа
Представьте, что нам надо записать число 1 миллиард. Самый очевидный путь:
let billion = 1000000000;
Мы также можем использовать символ нижнего подчёркивания _ в качестве разделителя:
let billion = 1_000_000_000
Символ нижнего подчёркивания _ – это «синтаксический сахар», он делает число более читабельным. Движок JavaScript попросту игнорирует _ между цифрами, поэтому в примере выше получается точно такой же миллиард, как и в первом случае.
Однако в реальной жизни мы в основном стараемся не писать длинные последовательности нулей, так как можно легко ошибиться. Укороченная запись может выглядеть как "1млрд" или "7.3млрд" для 7 миллиардов 300 миллионов. Такой принцип работает для всех больших чисел.
В JavaScript, чтобы укоротить запись числа, мы можем добавить к нему букву "e" и указать необходимое количество нулей:
Другими словами, "e" умножает число на 1 с указанным количеством нулей.
1e3 === 1 * 1000 // e3 означает *1000
1.23e6 === 1.23 * 1000000 // e6 означает *1000000
А сейчас давайте запишем что-нибудь очень маленькое. К примеру, 1 микросекунду (одна миллионная секунды):
let mcs = 0.000001;
В этом случае нам также поможет "e". Если мы хотим избежать записи длинной последовательности из нулей, мы можем сделать так:
let ms = 1e-6; // шесть нулей слева от 1
Если мы подсчитаем количество нулей в 0.000001, их будет 6. Естественно, верная запись 1e-6.
Другими словами, отрицательное число после "e" подразумевает деление на 1 с указанным количеством нулей:
// 1 делится на 1 с 3 нулями
1e-3 === 1 / 1000 (=0.001)
// 1.23 делится на 1 с 6 нулями
1.23e-6 === 1.23 / 1000000 (=0.00000123)
Шестнадцатеричные, двоичные и восьмеричные числа
Шестнадцатеричные числа широко используются в JavaScript для представления цветов, кодировки символов и многого другого. Естественно, есть короткий стиль записи: 0x, после которого указывается число.
Например:
Двоичные и восьмеричные числа используются не так часто, но они также поддерживаются: 0b для двоичных и 0o для восьмеричных:
Есть только 3 системы счисления с такой поддержкой. Для других систем счисления мы рекомендуем использовать функцию parseInt (рассмотрим позже в этой главе).
toString(base)
Метод num.toString(base) возвращает строковое представление числа num в системе счисления base.
Например:
base может варьироваться от 2 до 36 (по умолчанию 10).
Часто используемые:
-
base=16 — для шестнадцатеричного представления цвета, кодировки символов и т.д., цифры могут быть
0..9илиA..F. -
base=2 — обычно используется для отладки побитовых операций, цифры
0или1. -
base=36 — максимальное основание, цифры могут быть
0..9илиA..Z. То есть, используется весь латинский алфавит для представления числа. Забавно, но можно использовать36-разрядную систему счисления для получения короткого представления большого числового идентификатора. К примеру, для создания короткой ссылки. Для этого просто преобразуем его в36-разрядную систему счисления:
Внимание! Две точки в 123456..toString(36) это не опечатка. Если нам надо вызвать метод непосредственно на числе, как toString в примере выше, то нам надо поставить две точки .. после числа.
Если мы поставим одну точку: 123456.toString(36), тогда это будет ошибкой, поскольку синтаксис JavaScript предполагает, что после первой точки начинается десятичная часть. А если поставить две точки, то JavaScript понимает, что десятичная часть отсутствует, и начинается метод.
Также можно записать как (123456).toString(36).
Округление
Одна из часто используемых операций при работе с числами – это округление.
В JavaScript есть несколько встроенных функций для работы с округлением:
Math.floor- Округление в меньшую сторону:
3.1становится3, а-1.1—-2. Math.ceil- Округление в большую сторону:
3.1становится4, а-1.1—-1. Math.round- Округление до ближайшего целого:
3.1становится3,3.6—4, а-1.1—-1. Math.trunc(не поддерживается в Internet Explorer)- Производит удаление дробной части без округления:
3.1становится3, а-1.1—-1.
Ниже представлена таблица с различиями между функциями округления:
Math.floor |
Math.ceil |
Math.round |
Math.trunc |
|
|---|---|---|---|---|
3.1 |
3 |
4 |
3 |
3 |
3.6 |
3 |
4 |
4 |
3 |
-1.1 |
-2 |
-1 |
-1 |
-1 |
-1.6 |
-2 |
-1 |
-2 |
-1 |
Эти функции охватывают все возможные способы обработки десятичной части. Что если нам надо округлить число до n-ого количества цифр в дробной части?
Например, у нас есть 1.2345 и мы хотим округлить число до 2-х знаков после запятой, оставить только 1.23.
Есть два пути решения:
-
Умножить и разделить.
Например, чтобы округлить число до второго знака после запятой, мы можем умножить число на
100, вызвать функцию округления и разделить обратно. -
Метод toFixed(n) округляет число до
nзнаков после запятой и возвращает строковое представление результата.Округляет значение до ближайшего числа, как в большую, так и в меньшую сторону, аналогично методу
Math.round:Обратите внимание, что результатом
toFixedявляется строка. Если десятичная часть короче, чем необходима, будут добавлены нули в конец строки:Мы можем преобразовать полученное значение в число, используя унарный оператор
+илиNumber(), пример с унарным оператором:+num.toFixed(5).
Неточные вычисления
Внутри JavaScript число представлено в виде 64-битного формата IEEE-754. Для хранения числа используется 64 бита: 52 из них используется для хранения цифр, 11 для хранения положения десятичной точки и один бит отведён на хранение знака.
Если число слишком большое, оно переполнит 64-битное хранилище, JavaScript вернёт бесконечность:
Наиболее часто встречающаяся ошибка при работе с числами в JavaScript – это потеря точности.
Посмотрите на это (неверное!) сравнение:
Да-да, сумма 0.1 и 0.2 не равна 0.3.
Странно! Что тогда, если не 0.3?
Ой! Здесь гораздо больше последствий, чем просто некорректное сравнение. Представьте, вы делаете интернет-магазин и посетители формируют заказ из 2-х позиций за 0ドル.10 и 0ドル.20. Итоговый заказ будет 0ドル.30000000000000004. Это будет сюрпризом для всех.
Но почему это происходит?
Число хранится в памяти в бинарной форме, как последовательность бит – единиц и нулей. Но дроби, такие как 0.1, 0.2, которые выглядят довольно просто в десятичной системе счисления, на самом деле являются бесконечной дробью в двоичной форме.
Другими словами, что такое 0.1? Это единица делённая на десять — 1/10, одна десятая. В десятичной системе счисления такие числа легко представимы, по сравнению с одной третьей: 1/3, которая становится бесконечной дробью 0.33333(3).
Деление на 10 гарантированно хорошо работает в десятичной системе, но деление на 3 – нет. По той же причине и в двоичной системе счисления, деление на 2 обязательно сработает, а 1/10 становится бесконечной дробью.
В JavaScript нет возможности для хранения точных значений 0.1 или 0.2, используя двоичную систему, точно также, как нет возможности хранить одну третью в десятичной системе счисления.
Числовой формат IEEE-754 решает эту проблему путём округления до ближайшего возможного числа. Правила округления обычно не позволяют нам увидеть эту «крошечную потерю точности», но она существует.
Пример:
И когда мы суммируем 2 числа, их «неточности» тоже суммируются.
Вот почему 0.1 + 0.2 – это не совсем 0.3.
Справедливости ради заметим, что ошибка в точности вычислений для чисел с плавающей точкой сохраняется в любом другом языке, где используется формат IEEE 754, включая PHP, Java, C, Perl и Ruby.
Можно ли обойти проблему? Конечно, наиболее надёжный способ — это округлить результат используя метод toFixed(n):
Помните, что метод toFixed всегда возвращает строку. Это гарантирует, что результат будет с заданным количеством цифр в десятичной части. Также это удобно для форматирования цен в интернет-магазине 0ドル.30. В других случаях можно использовать унарный оператор +, чтобы преобразовать строку в число:
Также можно временно умножить число на 100 (или на большее), чтобы привести его к целому, выполнить математические действия, а после разделить обратно. Суммируя целые числа, мы уменьшаем погрешность, но она всё равно появляется при финальном делении:
Таким образом, метод умножения/деления уменьшает погрешность, но полностью её не решает.
Иногда можно попробовать полностью отказаться от дробей. Например, если мы в нашем интернет-магазине начнём использовать центы вместо долларов. Но что будет, если мы применим скидку 30%? На практике у нас не получится полностью избавиться от дроби. Просто используйте округление, чтобы отрезать «хвосты», когда надо.
Попробуйте выполнить его:
Причина та же – потеря точности. Из 64 бит, отведённых на число, сами цифры числа занимают до 52 бит, остальные 11 бит хранят позицию десятичной точки и один бит – знак. Так что если 52 бит не хватает на цифры, то при записи пропадут младшие разряды.
Интерпретатор не выдаст ошибку, но в результате получится «не совсем то число», что мы и видим в примере выше. Как говорится: «как смог, так записал».
Другим забавным следствием внутреннего представления чисел является наличие двух нулей: 0 и -0.
Все потому, что знак представлен отдельным битом, так что, любое число может быть положительным и отрицательным, включая нуль.
В большинстве случаев это поведение незаметно, так как операторы в JavaScript воспринимают их одинаковыми.
Проверка: isFinite и isNaN
Помните эти специальные числовые значения?
Infinity(и-Infinity) — особенное численное значение, которое ведёт себя в точности как математическая бесконечность ∞.NaNпредставляет ошибку.
Эти числовые значения принадлежат типу number, но они не являются «обычными» числами, поэтому есть функции для их проверки:
-
isNaN(value)преобразует значение в число и проверяет является ли оноNaN:Нужна ли нам эта функция? Разве не можем ли мы просто сравнить
=== NaN? К сожалению, нет. ЗначениеNaNуникально тем, что оно не является равным ничему другому, даже самому себе: -
isFinite(value)преобразует аргумент в число и возвращаетtrue, если оно является обычным числом, т.е. неNaN/Infinity/-Infinity:
Иногда isFinite используется для проверки, содержится ли в строке число:
Помните, что пустая строка интерпретируется как 0 во всех числовых функциях, включаяisFinite.
Number.isNaN и Number.isFiniteМетоды Number.isNaN и Number.isFinite – это более «строгие» версии функций isNaN и isFinite. Они не преобразуют аргумент в число, а наоборот – первым делом проверяют, является ли аргумент числом (принадлежит ли он к типу number).
-
Number.isNaN(value)возвращаетtrueтолько в том случае, если аргумент принадлежит к типуnumberи являетсяNaN. Во всех остальных случаях возвращаетfalse.alert( Number.isNaN(NaN) ); // true alert( Number.isNaN("str" / 2) ); // true // Обратите внимание на разный результат: alert( Number.isNaN("str") ); // false, так как "str" является строкой, а не числом alert( isNaN("str") ); // true, так как isNaN сначала преобразует строку "str" в число и в результате преобразования получает NaN -
Number.isFinite(value)возвращаетtrueтолько в том случае, если аргумент принадлежит к типуnumberи не являетсяNaN/Infinity/-Infinity. Во всех остальных случаях возвращаетfalse.alert( Number.isFinite(123) ); // true alert( Number.isFinite(Infinity) ); // false alert( Number.isFinite(2 / 0) ); // false // Обратите внимание на разный результат: alert( Number.isFinite("123") ); // false, так как "123" является строкой, а не числом alert( isFinite("123") ); // true, так как isFinite сначала преобразует строку "123" в число 123
Не стоит считать Number.isNaN и Number.isFinite более «корректными» версиями функций isNaN и isFinite. Это дополняющие друг-друга инструменты для разных задач.
Object.isСуществует специальный метод Object.is, который сравнивает значения примерно как ===, но более надёжен в двух особых ситуациях:
- Работает с
NaN:Object.is(NaN, NaN) === true, здесь он хорош. - Значения
0и-0разные:Object.is(0, -0) === false, это редко используется, но технически эти значения разные.
Во всех других случаях Object.is(a, b) идентичен a === b.
Этот способ сравнения часто используется в спецификации JavaScript. Когда внутреннему алгоритму необходимо сравнить 2 значения на предмет точного совпадения, он использует Object.is (Определение SameValue).
parseInt и parseFloat
Для явного преобразования к числу можно использовать + или Number(). Если строка не является в точности числом, то результат будет NaN:
Единственное исключение — это пробелы в начале строки и в конце, они игнорируются.
В реальной жизни мы часто сталкиваемся со значениями у которых есть единица измерения, например "100px" или "12pt" в CSS. Также во множестве стран символ валюты записывается после номинала "19€". Так как нам получить числовое значение из таких строк?
Для этого есть parseInt и parseFloat.
Они «читают» число из строки. Если в процессе чтения возникает ошибка, они возвращают полученное до ошибки число. Функция parseInt возвращает целое число, а parseFloat возвращает число с плавающей точкой:
Функции parseInt/parseFloat вернут NaN, если не смогли прочитать ни одну цифру:
parseInt(str, radix)Функция parseInt() имеет необязательный второй параметр. Он определяет систему счисления, таким образом parseInt может также читать строки с шестнадцатеричными числами, двоичными числами и т.д.:
Другие математические функции
В JavaScript встроен объект Math, который содержит различные математические функции и константы.
Несколько примеров:
Math.random()-
Возвращает псевдослучайное число в диапазоне от 0 (включительно) до 1 (но не включая 1)
Math.max(a, b, c...)/Math.min(a, b, c...)-
Возвращает наибольшее/наименьшее число из перечисленных аргументов.
Math.pow(n, power)-
Возвращает число
n, возведённое в степеньpower
В объекте Math есть множество функций и констант, включая тригонометрические функции, подробнее можно ознакомиться в документации по объекту Math.
Итого
Чтобы писать числа с большим количеством нулей:
- Используйте краткую форму записи чисел –
"e", с указанным количеством нулей. Например:123e6это123с 6-ю нулями123000000. - Отрицательное число после
"e"приводит к делению числа на 1 с указанным количеством нулей. Например:123e-6это0.000123(123миллионных).
Для других систем счисления:
- Можно записывать числа сразу в шестнадцатеричной (
0x), восьмеричной (0o) и бинарной (0b) системах счисления parseInt(str, base)преобразует строку в целое число в соответствии с указанной системой счисления:2 ≤ base ≤ 36.num.toString(base)представляет число в строковом виде в указанной системе счисленияbase.
Для проверки на NaN и Infinity:
isNaN(value)преобразует аргумент в число и проверяет, является ли оноNaNNumber.isNaN(value)проверяет, является ли аргумент числом, и если да, то проверяет, является ли оноNaNisFinite(value)преобразует аргумент в число и проверяет, что оно не являетсяNaN/Infinity/-InfinityNumber.isFinite(value)проверяет, является ли аргумент числом, и если да, то проверяет, что оно не являетсяNaN/Infinity/-Infinity
Для преобразования значений типа 12pt и 100px в число:
- Используйте
parseInt/parseFloatдля «мягкого» преобразования строки в число, данные функции по порядку считывают число из строки до тех пор пока не возникнет ошибка.
Для дробей:
- Используйте округления
Math.floor,Math.ceil,Math.trunc,Math.roundилиnum.toFixed(precision). - Помните, что при работе с дробями происходит потеря точности.
Ещё больше математических функций:
- Документация по объекту Math. Библиотека маленькая, но содержит всё самое важное.
Комментарии
<code>, для нескольких строк кода — тег<pre>, если больше 10 строк — ссылку на песочницу (plnkr, JSBin, codepen...)