◐ Shell
clean mode source ↗

Числа

У сучасному JavaScript існує два типи чисел:

  1. Звичайні числа в JavaScript, що зберігаються у 64-бітному форматі IEEE-754, також відомі як “подвійні точні числа з плаваючою комою”. Це числа, які ми використовуємо більшість часу, і про них ми поговоримо в цьому розділі.

  2. Числа 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 мікросекунда (одна мільйонна частина секунди):

Як і раніше, нам допоможе використання "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.

Є два способи зробити це:

  1. Помножити та розділити.

    Наприклад, щоб округлити число до другої цифри після десяткової крапки, ми можемо помножити число на 100, викликати функцію округлення і потім поділити його назад.

    let num = 1.23456;
    
    alert( Math.round(num * 100) / 100 ); // 1.23456 -> 123.456 -> 123 -> 1.23
  2. Метод 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.

Не тільки JavaScript

Ця ж проблема існує у багатьох інших мовах програмування.

PHP, Java, C, Perl, Ruby дають абсолютно однаковий результат, оскільки використовують один цифровий формат.

Чи можемо ми вирішити проблему? Звичайно, найнадійніший метод – округлення результату за допомогою методу toFixed(n):

let sum = 0.1 + 0.2;
alert( sum.toFixed(2) ); // "0.30"

Зауважте, що toFixed завжди повертає рядок, щоб число гарантовано мало дві цифри після десяткової крапки. Та й взагалі це зручно, якщо у нас є електронні покупки та нам потрібно показати $0.30. В інших випадках ми можемо використовувати одинарний плюс, якщо хочемо результат toFixed конвертувати в число:

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", '');

// буде true, якщо ви не введете Infinity, -Infinity чи NaN
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, який порівнює значення як ===, але є більш надійним для двох виняткових випадків:

  1. Працює з NaN: Object.is(NaN, NaN) === true, і це добре.
  2. Значення 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, коли вони вам потрібні. Бібліотека дуже мала, але охоплює основні потреби.