21 вересня 2023 р.

Базові оператори, математика

Зі шкільної програми ми знаємо багато арифметичних операцій, таких як додавання +, множення *, віднімання - тощо.

У цьому розділі ми почнемо з простих операторів, потім зосередимося на специфічних для JavaScript аспектах, які не охоплені шкільною арифметикою.

Терміни: “унарний”, “бінарний”, “операнд”

Перш ніж ми почнемо, розберімо певну загальну термінологію.

  • Операнд – це те, до чого застосовуються оператори. Наприклад, у множенні 5 * 2 є два операнди: лівий операнд 5 і правий операнд 2. Іноді їх називають “аргументами”, а не “операндами”.

  • Оператор є унарним, якщо він має один операнд. Наприклад, унарне заперечення - змінює знак числа:

    let x = 1;
    
    x = -x;
    alert( x ); // -1, було застосоване унарне заперечення
  • Оператор є бінарним, якщо він має два операнди. Наприклад, оператор мінус можна використовувати та у бінарній формі:

    let x = 1, y = 3;
    alert( y - x ); // 2, бінарний мінус віднімає значення

    Формально, у прикладах вище ми маємо два різні оператори, які позначаються однаковим символом: оператор заперечення – унарний оператор, який змінює знак числа, та оператор віднімання – бінарний оператор, який віднімає одне число від іншого.

Математика

JavaScript підтримує такі математичні операції:

  • Додавання +,
  • Віднімання -,
  • Множення *,
  • Ділення /,
  • Остача від ділення %,
  • Піднесення до степеня **.

Перші чотири операції зрозумілі, а от про % та ** потрібно сказати декілька слів.

Остача від ділення %

Оператор остачі %, попри свій зовнішній вигляд, не пов’язаний із відсотками.

Результатом a % b є остача цілочислового ділення a на b.

Наприклад:

alert( 5 % 2 ); // 1 - остача від ділення 5 на 2
alert( 8 % 3 ); // 2 - остача від ділення 8 на 3
alert( 8 % 4 ); // 0 - остача від ділення 8 на 4

Піднесення до степеня **

Оператор піднесення до степеня a ** b множить a саме на себе b разів.

У школі ми записуємо це як ab.

Наприклад:

alert( 2 ** 2 ); // 2² = 4
alert( 2 ** 3 ); // 2³ = 8
alert( 2 ** 4 ); // 2⁴ = 16

Так само як у математиці, оператор піднесення також можна використовувати для дробових чисел.

Наприклад, квадратний корінь це піднесення до степеня ½:

alert( 4 ** (1/2) ); // 2 (степінь 1/2 — це теж саме, що квадратний корінь)
alert( 8 ** (1/3) ); // 2 (степінь 1/3 — це теж саме, що кубічний корінь)

Об’єднання рядків через бінарний +

Розглянемо особливості операторів JavaScript, які виходять за межі шкільної арифметики.

Зазвичай оператор плюс + додає числа.

Але якщо бінарний + застосовується до рядків, він об’єднує їх:

let s = 'мій_' + 'рядок';
alert(s); // мій_рядок

Зверніть увагу, якщо будь-який з операндів є рядком, тоді інший також перетворюється на рядок.

Наприклад:

alert( '1' + 2 ); // "12"
alert( 2 + '1' ); // "21"

Бачите, не має значення, чи перший операнд – рядок, чи другий.

Ось складніший приклад:

alert(2 + 2 + '1' ); // "41", а не "221"

Тут оператори виконуються один за одним. Перший + додає два числа, тому він поверне 4; а наступний оператор + вже додасть (об’єднає) попередній результат із рядком 1. У підсумку ми отримаємо рядок '41' (4 + '1').

alert('1' + 2 + 2); // "122", а не "14"

У цьому прикладі перший операнд – рядок, тому компілятор також опрацьовує інші два операнди як рядки. Операнд 2 приєднується (конкатенується) до '1', тому в результаті буде '1' + 2 = "12", а потім — "12" + 2 = "122".

Лише бінарний + працює з рядками так. Інші арифметичні оператори працюють тільки з числами й завжди перетворюють свої операнди на числа.

Ось приклад, як працює віднімання й ділення:

alert( 6 - '2' ); // 4, '2' перетворюється на число
alert( '6' / '2' ); // 3, обидва операнди перетворюються на числа

Числове перетворення, унарний +

У оператора плюс + є дві форми: бінарна, яку ми використовували вище, та унарна.

Унарний плюс або, іншими словами, оператор плюс +, застосований до одного операнда, нічого не зробить, якщо операнд є числом. Але якщо операнд не є числом, унарний плюс перетворить його на число.

Наприклад:

// Нема ніякого впливу на числа
let x = 1;
alert( +x ); // 1

let y = -2;
alert( +y ); // -2

// Перетворює нечислові значення
alert( +true ); // 1
alert( +"" );   // 0

Він насправді працює як і Number(...), але має коротший вигляд.

Необхідність перетворення рядків на числа виникає дуже часто. Наприклад, якщо ми отримуємо значення з полів HTML форми, вони зазвичай є рядками. Що робити, якщо ми хочемо їх підсумувати?

Бінарний плюс додав би їх як рядки:

let apples = "2";
let oranges = "3";

alert( apples + oranges ); // "23", бінарний плюс об’єднує рядки

Якщо ми хочемо використовувати їх як числа, нам потрібно конвертувати, а потім підсумувати їх:

let apples = "2";
let oranges = "3";

// обидва значення перетворюються на числа перед застосуванням бінарного плюса
alert( +apples + +oranges ); // 5

// довший варіант
// alert( Number(apples) + Number(oranges) ); // 5

З погляду математика надмірні плюси можуть здатися дивними. Але з погляду програміста тут немає нічого особливого: спочатку застосовуються унарні плюси, вони перетворюють рядки на числа, а потім бінарний плюс підсумовує їх.

Чому унарні плюси застосовуються до значень перед бінарними плюсами? Як ми побачимо далі, це пов’язано з їхнім вищим пріоритетом.

Пріоритет оператора

Якщо вираз має більше одного оператора, порядок виконання визначається їхнім пріоритетом, або, іншими словами, типовим порядком першості операторів.

Зі школи ми всі знаємо, що множення у виразі 1 + 2 * 2 має бути обчислене перед додаванням. Саме це і є пріоритетом. Кажуть, що множення має вищий пріоритет, ніж додавання.

Дужки перевизначають будь-який пріоритет, тому, якщо ми не задоволені типовим пріоритетом, ми можемо використовувати дужки, щоби змінити його. Наприклад: (1 + 2) * 2.

У JavaScript є багато операторів. Кожен оператор має відповідний номер пріоритету. Першим виконується той оператор, який має найбільший номер пріоритету. Якщо пріоритет є однаковим, порядок виконання — зліва направо.

Ось витяг із таблиці пріоритетів (вам не потрібно її запам’ятовувати, але зверніть увагу, що унарні оператори мають вищий пріоритет за відповідні бінарні):

Пріоритет Ім’я Знак
14 унарний плюс +
14 унарний мінус -
13 піднесення до степеня **
12 множення *
12 ділення /
11 додавання +
11 віднімання -
2 присвоєння =

Як ми бачимо, “унарний плюс” має пріоритет 14, що вище за 11 – пріоритет “додавання” (бінарний плюс). Саме тому, у виразі "+apples + +oranges", унарні плюси виконуються перед додаванням (бінарним плюсом).

Присвоєння

Зазначимо, що присвоєння = також є оператором. Воно є у таблиці з пріоритетами й має дуже низький пріоритет 2.

Тому, коли ми присвоюємо значення змінній, наприклад, x = 2 * 2 + 1, спочатку виконуються обчислення, а потім виконується присвоєння = зі збереженням результату в x.

let x = 2 * 2 + 1;

alert( x ); // 5

Присвоєння = повертає результат

Той факт, що = є оператором, а не “магічною” конструкцією мови, має цікаве значення.

Усі оператори в JavaScript повертають значення. Це очевидно для + та -, але це також правдиво для =.

Виклик x = значення записує значення у x, а потім повертає його.

Ось демонстрація, яка використовує присвоєння як частину складнішого виразу:

let a = 1;
let b = 2;

let c = 3 - (a = b + 1);

alert( a ); // 3
alert( c ); // 0

У наведеному вище прикладі результат виразу (a = b + 1) є значенням, яке присвоювалося змінній a (тобто 3). Потім воно використовується для подальших обчислень.

Чудернацький код, чи не так? Ми маємо розуміти, як це працює, бо іноді ми бачимо подібне в бібліотеках JavaScript.

Однак, будь ласка, не пишіть свій код так. Ці трюки, безумовно, не роблять код більш зрозумілим або читабельним.

Ланцюгові присвоєння

Іншою цікавою особливістю є здатність ланцюгового присвоєння:

let a, b, c;

a = b = c = 2 + 2;

alert( a ); // 4
alert( b ); // 4
alert( c ); // 4

Ланцюгове присвоєння виконується справа наліво. Спочатку обчислюється найправіший вираз 2 + 2, а потім результат присвоюється змінним ліворуч: c, b та a. Зрештою всі змінні мають спільне значення.

Знову таки, щоби покращити читабельність коду, краще розділяти подібні конструкції на декілька рядків:

c = 2 + 2;
b = c;
a = c;

Так легше прочитати, особливо коли швидко переглядати код.

Оператор “модифікувати та присвоїти”

Часто нам потрібно застосувати оператор до змінної й зберегти новий результат у ту ж саму змінну.

Наприклад:

let n = 2;
n = n + 5;
n = n * 2;

Цей запис можна скоротити за допомогою операторів += та *=:

let n = 2;
n += 5; // тепер n = 7 (те ж саме, що n = n + 5)
n *= 2; // тепер n = 14 (те ж саме, що n = n * 2)

alert( n ); // 14

Короткі оператори “модифікувати та присвоїти” є для всіх арифметичних та побітових операторів: /=, -= тощо.

Ці оператори мають такий же пріоритет, як і звичайне присвоєння, тому вони виконуються після більшості інших обчислень:

let n = 2;

n *= 3 + 5; // права частина обчислюється першою, так само як і n *= 8

alert( n ); // 16

Інкремент/декремент

Збільшення або зменшення на одиницю є однією з найпоширеніших числових операцій.

Тому для цього є спеціальні оператори:

  • Інкремент ++ збільшує змінну на 1:

    let counter = 2;
    counter++;        // працює так само, як counter = counter + 1, але запис коротше
    alert( counter ); // 3
  • Декремент -- зменшує змінну на 1:

    let counter = 2;
    counter--;        // працює так само, як counter = counter - 1, але запис коротше
    alert( counter ); // 1
Важливо:

Інкремент/декремент можуть застосовуватися лише до змінних. Спроба використати їх із значенням, як от 5++, призведе до помилки.

Оператори ++ та -- можуть розташовуватися до або після змінної.

  • Коли оператор йде за змінною, він у “постфіксній формі”: counter++.
  • “Префіксна форма” – це коли оператор йде попереду змінної: ++counter.

Обидві ці інструкції роблять те ж саме: збільшують counter на 1.

Чи є різниця? Так, але ми можемо побачити її тільки використавши значення, яке повертають ++/--.

Розберімось. Як нам відомо, всі оператори повертають значення. Інкремент/декремент не є винятком. Префіксна форма повертає нове значення, тоді як постфіксна форма повертає старе значення (до збільшення/зменшення).

Щоби побачити різницю, наведемо приклад:

let counter = 1;
let a = ++counter; // (*)

alert(a); // 2

У рядку (*), префіксна форма ++counter збільшує counter та повертає нове значення, 2. Отже, alert показує 2.

Тепер скористаємося постфіксною формою:

let counter = 1;
let a = counter++; // (*) змінили ++counter на counter++

alert(a); // 1

У рядку (*), постфіксна форма counter++ також збільшує counter, але повертає старе значення (до інкременту). Отже, alert показує 1.

Підсумки:

  • Якщо результат збільшення/зменшення не використовується, немає ніякої різниці, яку форму використовувати:

    let counter = 0;
    counter++;
    ++counter;
    alert( counter ); // 2, у рядках вище робиться одне і те ж саме
  • Якщо ми хочемо збільшити значення та негайно використати результат оператора, нам потрібна префіксна форма:

    let counter = 0;
    alert( ++counter ); // 1
  • Якщо ми хочемо збільшити значення, але використати його попереднє значення, нам потрібна постфіксна форма:

    let counter = 0;
    alert( counter++ ); // 0
Інкремент/декремент серед інших операторів

Оператори ++/-- також можуть використовуватися всередині виразів. Їхній пріоритет вищий за більшість інших арифметичних операцій.

Наприклад:

let counter = 1;
alert( 2 * ++counter ); // 4

Порівняйте з:

let counter = 1;
alert( 2 * counter++ ); // 2, тому що counter++ повертає "старе" значення

Хоча з технічного погляду це допустимо, такий запис робить код менш читабельним. Коли один рядок робить кілька речей – це не добре.

Під час читання коду швидке “вертикальне” сканування оком може легко пропустити щось подібне до counter++, і не буде очевидним, що змінна була збільшена.

Ми рекомендуємо стиль “одна лінія – одна дія”:

let counter = 1;
alert( 2 * counter );
counter++;

Побітові оператори

Побітові оператори розглядають аргументи як 32-бітні цілі числа та працюють на рівні їхнього двійкового представлення.

Ці оператори не є специфічними для JavaScript. Вони підтримуються у більшості мов програмування.

Список операторів:

  • AND(і) ( & )
  • OR(або) ( | )
  • XOR(побітове виключне або) ( ^ )
  • NOT(ні) ( ~ )
  • LEFT SHIFT(зсув ліворуч) ( << )
  • RIGHT SHIFT(зсув праворуч) ( >> )
  • ZERO-FILL RIGHT SHIFT(зсув праворуч із заповненням нулями) ( >>> )

Ці оператори використовуються тоді, коли нам потрібно “возитися” з числами на дуже низькому (побітовому) рівні (тобто – вкрай рідко). Найближчим часом такі оператори нам не знадобляться, оскільки у веброзробці вони майже не використовуються. Проте в таких галузях, як криптографія, вони можуть бути дуже корисними. Ви можете прочитати розділ Bitwise Operators на MDN, якщо виникне потреба.

Кома

Оператор “кома” (,) незвичайний і застосовується дуже рідко. Іноді цей оператор використовують для написання коротшого коду, тому нам потрібно знати його, щоби розуміти, що відбувається.

Оператор кома дає змогу обчислити кілька виразів, розділивши їх комою ,. Кожен із них обчислюється, але повертається тільки результат останнього.

Наприклад:

let a = (1 + 2, 3 + 4);

alert( a ); // 7 (результат обчислення 3 + 4)

Тут обчислюється перший вираз 1 + 2 і його результат викидається. Потім обчислюється 3 + 4 і повертається як результат.

Кома має дуже низький пріоритет

Зверніть увагу, що оператор “кома” має дуже низький пріоритет, нижчий за =, тому дужки є важливими в наведеному вище прикладі.

Без дужок, у виразі a = 1 + 2, 3 + 4 спочатку обчислюються оператори +, підсумовуючи числа у a = 3, 7; потім оператор присвоєння = присвоює a = 3, а решта (число 7 після коми) ігнорується. Це як записати вираз (a = 1 + 2), 3 + 4.

Чому нам потрібен оператор, що викидає все, окрім останнього виразу?

Іноді його використовують у складніших конструкціях, щоби помістити кілька дій в один рядок.

Наприклад:

// три операції в одному рядку
for (a = 1, b = 3, c = a * b; a < 10; a++) {
 ...
}

Такі трюки використовуються в багатьох фреймворках JavaScript. Саме тому ми їх згадуємо. Але зазвичай вони не покращують читабельність коду, тому ми маємо добре подумати перед їх використанням.

Завдання

важливість: 5

Які кінцеві значення всіх змінних a, b, c та d після виконання коду нижче?

let a = 1, b = 1;

let c = ++a; // ?
let d = b++; // ?

Відповідь:

  • a = 2
  • b = 2
  • c = 2
  • d = 1
let a = 1, b = 1;

alert( ++a ); // 2, префіксна форма повертає нове значення
alert( b++ ); // 1, постфіксна форма повертає старе значення

alert( a ); // 2, збільшується один раз
alert( b ); // 2, збільшується один раз
важливість: 3

Які значення мають a та x після виконання коду нижче?

let a = 2;

let x = 1 + (a *= 2);

Відповідь:

  • a = 4 (помножиться на 2)
  • x = 5 (обчислюється як 1 + 4)
важливість: 5

Які результати цих виразів?

"" + 1 + 0
"" - 1 + 0
true + false
6 / "3"
"2" * "3"
4 + 5 + "px"
"$" + 4 + 5
"4" - 2
"4px" - 2
"  -9  " + 5
"  -9  " - 5
null + 1
undefined + 1
" \t \n" - 2

Добре подумайте, запишіть, а потім порівняйте з відповіддю.

"" + 1 + 0 = "10" // (1)
"" - 1 + 0 = -1 // (2)
true + false = 1
6 / "3" = 2
"2" * "3" = 6
4 + 5 + "px" = "9px"
"$" + 4 + 5 = "$45"
"4" - 2 = 2
"4px" - 2 = NaN
"  -9  " + 5 = "  -9  5" // (3)
"  -9  " - 5 = -14 // (4)
null + 1 = 1 // (5)
undefined + 1 = NaN // (6)
" \t \n" - 2 = -2 // (7)
  1. Додавання пустого рядка "" + 1 перетворює число 1 на рядок: "" + 1 = "1"; далі ми маємо "1" + 0, де застосовується те ж саме правило.
  2. Віднімання - (як і більшість математичних операцій) працює тільки з числами, воно перетворює порожній рядок "" на 0.
  3. Додавання з рядком додає число 5 до рядка.
  4. Віднімання завжди перетворює на числа, тому рядок " -9 " перетвориться на число -9 (ігноруючи пробіли навколо нього).
  5. null стає 0 після числового перетворення.
  6. undefined стає NaN після числового перетворення.
  7. Символи пробілів по краях рядка ігноруються під час перетворення в число. Тому рядок, який містить лише символи \t, \n або «звичайні» пробіли, прирівнюється до пустого рядка і стає 0 після числового перетворення.
важливість: 5

Нижче наведено код, що просить користувача ввести два числа і відображає їхню суму.

Він працює неправильно. Код у прикладі виводить 12 (для початкових значень у полях вводу).

У чому помилка? Виправте її. Результат має бути 3.

let a = prompt("Перше число?", 1);
let b = prompt("Друге число?", 2);

alert(a + b); // 12

Причина в тому, що вікно запиту повертає ввід користувача як рядок.

Отже, змінні отримують значення "1" і "2" відповідно.

let a = "1"; // prompt("Перше число?", 1);
let b = "2"; // prompt("Друге число?", 2);

alert(a + b); // 12

Нам треба перетворити рядки на числа перед застосуванням оператора +. Наприклад, за допомогою Number() або вставлення + перед ними.

Вставити + можна безпосередньо перед prompt:

let a = +prompt("Перше число?", 1);
let b = +prompt("Друге число?", 2);

alert(a + b); // 3

Або всередині alert:

let a = prompt("Перше число?", 1);
let b = prompt("Друге число?", 2);

alert(+a + +b); // 3

В останньому варіанті унарний і бінарний + використовуються разом. Виглядає химерно, чи не так?

Навчальна карта