У сучасному JavaScript існує два типи чисел:
-
Звичайні числа в JavaScript, що зберігаються у 64-бітному форматі IEEE-754, також відомі як “подвійні точні числа з плаваючою комою”. Це числа, які ми використовуємо більшість часу, і про них ми поговоримо в цьому розділі.
-
Числа BigInt, для відображення цілих чисел довільної довжини. Іноді вони потрібні, оскільки звичайне число не може безпечно перевищувати
(253-1)
або бути менше ніж-(253-1)
, як ми згадували раніше в розділі Типи даних. Оскільки числа BigInt використовуються в декількох спеціальних областях, їм присвячено окремий розділ BigInt.
То ж тут ми поговоримо про звичайні числа. Поглибимо наші знання про них.
Більше способів написання числа
Уявіть, нам потрібно написати 1 мільярд. Прямий спосіб це:
let billion = 1000000000;
Також можна використовувати знак підкреслення (нижню риску) _
в якості розділювача:
let billion = 1_000_000_000;
В цьому випадку знак підкреслення _
відіграє роль “синтаксичного цукру”, він робить число більш читабельним. Рушій JavaScript просто ігнорує _
між цифрами, тому для нього це мільярд, як і в прикладі вище.
В реальному житті ми намагаємося уникати написання довгих рядків з нулями. Ми надто ліниві для цього. Зазвичай ми напишемо щось на кшталт "1 млрд"
для мільярда або "7.3 млрд"
для 7 мільярдів 300 мільйонів. Те саме стосується більшості великих чисел.
У JavaScript можна скоротити число, додавши букву "е"
та кількість нулів після неї:
let billion = 1e9; // 1 мільярд, буквально: 1 та 9 нулів
alert( 7.3e9 ); // 7.3 мільярдів (таке ж саме що 7300000000 чи 7_300_000_000)
Іншими словами, "e"
помножує число на 1
із заданим числом нулів.
1e3 === 1 * 1000; // e3 означає *1000
1.23e6 === 1.23 * 1000000; // e6 означає *1000000
Тепер напишемо щось дуже маленьке. Наприклад, 1 мікросекунда (одна мільйонна частина секунди):
let mсs = 0.000001;
Як і раніше, нам допоможе використання "e"
. Якщо ми хочемо уникнути явного запису нулів, ми можемо написати:
let mcs = 1e-6; // шість нулів зліва від 1
Якщо порахувати нулі в 0.000001
, їх буде 6. Так що, цілком очікувано, що це 1e-6
.
Іншими словами, від’ємне число після "е"
означає ділення на 1 з заданою кількістю нулів:
// -3 ділиться на 1 з 3 нулями
1e-3 === 1 / 1000; // 0.001
// -6 ділиться на 1 з 6 нулями
1.23e-6 === 1.23 / 1000000; // 0.00000123
// an example with a bigger number
1234e-2 === 1234 / 100; // 12.34, decimal point moves 2 times
Двійкові, вісімкові та шістнадцяткові числа
Шістнадцяткові числа широко використовуються в JavaScript для представлення кольорів, кодування символів та багатьох інших речей. Тому, цілком очікувано, що існує коротший спосіб їх написання: 0x
, а потім саме число.
Наприклад:
alert( 0xff ); // 255
alert( 0xFF ); // 255 (те саме, регістр не має значення)
Двійкові та вісімкові системи числення рідко використовуються, але також підтримуються за допомогою префіксів “0b” і “0o”:
let a = 0b11111111; // двійкова форма 255
let b = 0o377; // вісімкова форма 255
alert( a == b ); // true, те саме число 255 з обох сторін
Є лише 3 системи числення з такою підтримкою. Для інших систем числення ми повинні використовувати функцію parseInt
(яку ми побачимо далі в цьому розділі).
toString(base)
Метод num.toString(base)
повертає рядкове представлення num
в системі числення із заданим base
.
Наприклад:
let num = 255;
alert( num.toString(16) ); // ff
alert( num.toString(2) ); // 11111111
base
може бути від 2
до 36
. За замовчуванням це 10
.
Загальні випадки використання для цього є:
-
base=16 використовується для шістнадцяткових кольорів, кодування символів тощо, цифри можуть бути
0..9
абоA..F
. -
base=2 в основному для налагодження бітових операцій, цифри можуть бути
0
або1
. -
base=36 є максимальним, цифри можуть бути
0..9
абоA..Z
. Весь латинський алфавіт використовується для позначення числа. Комічно, але користь від системи для найбільших чисел полягає у перетворенні довгого числового ідентифікатора у щось коротше, наприклад, для генерації короткого URL. Для цього достатньо представити його в системі числення з базою36
:alert( 123456..toString(36) ); // 2n9c
Зверніть увагу, що дві крапки в 123456..toString(36)
– це не помилка. Якщо ми хочемо викликати метод безпосередньо на число, наприклад toString
у наведеному вище прикладі, тоді нам потрібно поставити дві крапки ..
після нього.
Якби ми помістили одну крапку: 123456.toString(36)
, тоді виникла б помилка, оскільки синтаксис JavaScript передбачає десяткову частину після першої точки. І якщо ми розмістимо ще одну крапку, то JavaScript розпізнає, що десяткова частина порожня, і далі йде метод.
Також можна написати (123456).toString(36)
.
Округлення
Однією з найбільш використовуваних операцій при роботі з числами є округлення.
Існує кілька вбудованих функцій для округлення:
Math.floor
- Округляє вниз:
3.1
стає3
, та-1.1
стає-2
. Math.ceil
- Округляє вверх:
3.1
стає4
, та-1.1
стає-1
. Math.round
- Округляє до найближчого цілого числа:
3.1
стає3
,3.6
стає4
,3.5
теж округлить до4
. 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
і хочете округлити його до двох цифр, щоб отримати 1.23
.
Є два способи зробити це:
-
Помножити та розділити.
Наприклад, щоб округлити число до другої цифри після десяткової крапки, ми можемо помножити число на
100
, викликати функцію округлення і потім поділити його назад.let num = 1.23456; alert( Math.round(num * 100) / 100 ); // 1.23456 -> 123.456 -> 123 -> 1.23
-
Метод toFixed(n) округляє число до
n
цифр після точки та повертає рядкове представлення результату.let num = 12.34; alert( num.toFixed(1) ); // "12.3"
Це округляє вгору або вниз до найближчого значення, подібно до
Math.round
:let num = 12.36; alert( num.toFixed(1) ); // "12.4"
Зверніть увагу, що результат
toFixed
– це рядок. Якщо десяткова частина коротша, ніж потрібно, нулі додаються в кінці:let num = 12.34; alert( num.toFixed(5) ); // "12.34000", додано нулі, щоб зробити рівно 5 цифр
Ми можемо перетворити його на число, використовуючи унарний плюс
+num.toFixed(5)
абоNumber()
.
Неточні розрахунки
Із середини, число представлено у 64-бітному форматі IEEE-754, тому для його зберігання треба саме 64 біти: 52 з них використовуються для зберігання цифр, 11 – відповідають за позицію десяткової крапки (для цілих чисел вони дорівнюють нулю), а 1 біт – для знака.
Якщо число занадто велике, та переповнює 64-біти, воно буде перетворене на спеціальне числове значення Infinity
(Нескінченність):
alert( 1e500 ); // Infinity
Що може бути трохи менш очевидним, але трапляється досить часто, це втрата точності.
Розглянемо цей (хибний!) тест:
alert( 0.1 + 0.2 == 0.3 ); // false
Все вірно, якщо ми перевіримо, чи сума 0.1
та 0.2
дорівнює 0.3
, отримаємо false
.
Дивно! Що це тоді, якщо не 0.3
?
alert( 0.1 + 0.2 ); // 0.30000000000000004
Оце так! Уявіть, що ви робите вебсайт для електронних покупок, і відвідувач кладе в кошик товари $0.10
та $0.20
. Загальна сума замовлення складе $0.30000000000000004
. Це може здивувати будь-кого.
Але чому так відбувається?
Число зберігається в пам’яті у його двійковій формі, послідовність бітів – одиниць і нулів. Але дроби на кшталт 0.1
, 0.2
, які виглядають просто в десятковій системі числення, насправді є нескінченними дробами у своїй двійковій формі.
Іншими словами, що таке 0.1
? Це одиниця розділена на десять 1/10
– одна десята. У десятковій системі такі числа досить легко представити, але якщо порівняти його з однією третиною: 1/3
, то ми стикаємось з нескінченним дробом 0.33333(3)
.
Отже, поділ на 10
гарантовано працює в десятковій системі, але поділ на 3
– ні. З цієї ж причини в системі двійкових чисел поділ на 2
гарантовано працює, але 1/10
стає нескінченним двійковим дробом.
Просто немає можливості зберігати рівно 0.1 або рівно 0.2 за допомогою двійкової системи, так само як немає можливості зберігати одну третю, як десятковий дріб.
Числовий формат IEEE-754 вирішує це шляхом округлення до найближчого можливого числа. Ці правила округлення зазвичай не дозволяють нам побачити “крихітні втрати точності”, але вони існують.
Ми можемо побачити це на прикладі:
alert( 0.1.toFixed(20) ); // 0.10000000000000000555
І коли ми підсумовуємо два числа, їх “втрати на точність” складаються.
Ось чому 0.1 + 0.2
не є 0.3
.
Ця ж проблема існує у багатьох інших мовах програмування.
PHP, Java, C, Perl, Ruby дають абсолютно однаковий результат, оскільки використовують один цифровий формат.
Чи можемо ми вирішити проблему? Звичайно, найнадійніший метод – округлення результату за допомогою методу toFixed(n):
let sum = 0.1 + 0.2;
alert( sum.toFixed(2) ); // "0.30"
Зауважте, що toFixed
завжди повертає рядок, щоб число гарантовано мало дві цифри після десяткової крапки. Це насправді зручно, якщо у нас є електронні покупки та нам потрібно показати $0.30
. В інших випадках ми можемо використовувати одинарний плюс, щоб для приведення його до числа:
let sum = 0.1 + 0.2;
alert( +sum.toFixed(2) ); // 0.3
Ми також можемо тимчасово помножити числа на 100 (або більше число), щоб перетворити їх на цілі числа, виконати математичні операції і поділити назад. Якщо ми робимо розрахунки з цілими числами, помилка дещо зменшується, але ми все одно отримуємо її при діленні:
alert( (0.1 * 10 + 0.2 * 10) / 10 ); // 0.3
alert( (0.28 * 100 + 0.14 * 100) / 100); // 0.4200000000000001
Отже, підхід множення/ділення зменшує помилку, але не видаляє її повністю.
Іноді можна спробувати уникнути проблем з дробами. Якщо ми маємо справу з магазином, то можемо зберігати ціни в центах замість доларів. Але що робити, якщо ми застосуємо знижку в розмірі 30%? На практиці повністю уникнути дробів вдається досить рідко. Просто округліть їх, щоб вирізати “хвости”, коли це потрібно.
Спробуйте запустити:
// Привіт! Я число, що збільшується само по собі
alert( 9999999999999999 ); // покаже 10000000000000000
Це ще один приклад тієї ж проблеми: втрата точності. Для числа існує 64 біти, 52 з них можна використовувати для зберігання цифр, але цього недостатньо. Так зникають найменш значущі цифри.
JavaScript не викликає помилку в таких випадках. Він робить все можливе, щоб число відповідало бажаному формату, та на жаль, цей формат недостатньо великий.
Ще одним кумедним наслідком внутрішньої реалізації чисел є наявність двох нулів: 0
і -0
.
Це тому, що знак представлений одним бітом, тому його можна встановити або не встановити для будь-якого числа, включаючи нуль.
У більшості випадків відмінність непомітна, оскільки оператори підходять до них як до однакових.
Перевірки: isFinite та isNaN
Пам’ятаєте ці два особливі числові значення?
Infinity
(та-Infinity
) – це особливе числове значення, яке більше (менше) ніж усе.NaN
представляє помилку.
Вони належать до типу number
, але не є “нормальними” числами, тому для їх перевірки існують спеціальні функції:
-
isNaN(value)
перетворює свій аргумент у число, а потім перевіряє його на належність доNaN
:alert( isNaN(NaN) ); // true alert( isNaN("str") ); // true
Але чи потрібна нам ця функція? Чи не можемо ми просто використати порівняння
=== NaN
? Вибачте, але відповідь – ні. ЗначенняNaN
унікальне тим, що воно нічому не дорівнює, включаючи себе:alert( NaN === NaN ); // false
-
isFinite(value)
перетворює свій аргумент в число і повертаєtrue
, якщо це звичайне число, таfalse
, якщоNaN/Infinity/-Infinity
:alert( isFinite("15") ); // true alert( isFinite("str") ); // false, тому що це спеціальне значення: NaN alert( isFinite(Infinity) ); // false, тому що це спеціальне значення: Infinity
Іноді isFinite
використовується для перевірки того, чи є значення рядка звичайним числом:
let num = +prompt("Enter a number", '');
// буде істинним, якщо ви не введете Infinity, -Infinity чи не число
alert( isFinite(num) );
Зауважте, що порожній рядок, або рядок з пробілів трактується як 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
. Однак на практиці переважно використовуються 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
.
Ми згадуємо тут Object.is
, оскільки він часто використовується в специфікації JavaScript. Коли для внутрішнього алгоритму потрібно порівняти два значення, щоб вони були абсолютно однаковими, він використовує Object.is
(ще його називають SameValue).
parseInt та parseFloat
Числове перетворення за допомогою плюса +
або Number()
є суворим, тож якщо значення не є гарантованим числом, то станеться помилка:
alert( +"100px" ); // NaN
Винятком є пробіли на початку або в кінці рядка, оскільки вони ігноруються.
Але в реальному житті ми часто маємо значення в конкретних одиницях, наприклад, "100px"
або "12pt"
в CSS. Також у багатьох країнах символ валюти йде після значення, тому у нас є "19€"
і ми хочемо отримати число з цього.
Ось для чого призначені parseInt
та parseFloat
.
Вони “читають” число з рядка, до поки можуть, у разі помилки зчитане число повертається. Функція parseInt
повертає ціле число, тоді як parseFloat
повертає число з плаваючою крапкою:
alert( parseInt('100px') ); // 100
alert( parseFloat('12.5em') ); // 12.5
alert( parseInt('12.3') ); // 12, тільки частина цілого числа
alert( parseFloat('12.3.4') ); // 12.3, друга крапка зупиняє зчитування
Бувають ситуації, в яких parseInt/parseFloat
повернуть NaN
, коли не вдалось прочитати жодної цифри:
alert( parseInt('a123') ); // NaN, перший символ зупиняє процес
parseInt(str, radix)
Функція parseInt()
має необов’язковий другий аргумент. Він вказує основу системи числення, тому parseInt
також може проаналізувати рядки шістнадцяткових, двійкових та інших чисел:
alert( parseInt('0xff', 16) ); // 255
alert( parseInt('ff', 16) ); // 255, без 0x також працює
alert( parseInt('2n9c', 36) ); // 123456
Інші математичні функції
JavaScript має вбудований Math об’єкт, який містить невелику бібліотеку математичних функцій та констант.
Декілька прикладів:
Math.random()
-
Повертає випадкове число від 0 до 1 (не включаючи 1).
alert( Math.random() ); // 0.1234567894322 alert( Math.random() ); // 0.5435252343232 alert( Math.random() ); // ... (будь-яке випадкове число)
Math.max(a, b, c...)
/Math.min(a, b, c...)
-
Повертає найбільше/найменше число з довільної кількості аргументів.
alert( Math.max(3, 5, -10, 0, 1) ); // 5 alert( Math.min(1, 2) ); // 1
Math.pow(n, power)
-
Повертає
n
, зведене у ступіньpower
.alert( Math.pow(2, 10) ); // 2 у ступені 10 = 1024
Об’єкт Math
включає ще багато функцій і констант, в тому числі тригонометрію. Детальніше про об’єкт Math
можна почитати в документації.
Підсумки
Щоб записати числа з багатьма нулями:
- Додайте
"e"
з числом нулів до числа. Як і:123e6
те саме, що123
з 6 нулями123000000
. - Від’ємне число після
"е"
призводить до ділення числа на 1 із заданими нулями. Наприклад123e-6
означає0.000123
(123
мільйони).
Для різних систем числення:
- Можна записувати числа безпосередньо в шістнадцятковій (
0x
), вісімковій (0o
) та двійковій (0b
) системах. parseInt(str, base)
розбирає рядокstr
на ціле число чисельної системи із заданимbase
,2 ≤ base ≤ 36
.num.toString(base)
перетворює число в рядок в системі числення за допомогою заданоїbase
.
Для регулярних тестів чисел:
isNaN(value)
перетворює свій аргумент на число, а потім перевіряє його наNaN
Number.isNaN(value)
перевіряє, чи належить його аргумент до типуnumber
, і якщо так, перевіряє його наNaN
isFinite(value)
перетворює свій аргумент на число, а потім перевіряє, чи не єNaN/Infinity/-Infinity
Number.isFinite(value)
перевіряє, чи належить його аргумент до типуnumber
, і якщо так, перевіряє, чи не єNaN/Infinity/-Infinity
Для перетворення значень на зразок 12pt
та 100px
у число:
- Використовуйте
parseInt/parseFloat
для “не суворого” перетворення, яке зчитує число з рядка, а потім повертає значення, яке вдалося прочитати перед помилкою.
Для дробів:
- Округлюйте за допомогою
Math.floor
,Math.ceil
,Math.trunc
,Math.round
абоnum.toFixed(precision)
. - Пам’ятайте, що при роботі з дробами втрачається точність.
Більше математичних функцій:
- Дивіться об’єкт Math, коли вони вам потрібні. Бібліотека дуже мала, але охоплює основні потреби.