JavaScript — дивний. Не вірите? Тоді спробуйте перетворити масив рядків у цілочислові значення, використовуючи map
та parseInt
. Запустіть вашу консоль (F12 для Chrome), вставте наведений нижче код та натисніть Enter.
['1', '7', '11'].map(parseInt);
Замість того щоб отримати масив цілих чисел [1, 7, 11]
, ми бачимо [1, NaN, 3]
. Неочікувано? Щоб розібратися, що ж насправді тут відбувається, оглянемо спершу деякі концепції JavaScript. Якщо ж вам потрібен TLDR — він наприкінці матеріалу.
Правдиві та хибні значення
Поглянемо на добре знайомий умовний вираз в JavaScript:
if (true) {
// цей код завжди виконується
} else {
// ніколи не виконається
}
У цьому прикладі умова оператора завжди істинна, тому if-блок завжди виконується, а else-блок ігнорується. Ми розглянули лише найпростіший приклад, адже true
— булеве значення. Як йдуть справи зі значеннями інших типів?
if ("hello world") {
// це запуститься?
console.log("Condition is truthy");
} else {
// чи це?
console.log("Condition is falsy");
}
Знову звернемося до консолі розробника (F12 для Chrome). Побачимо, що виконується if-блок. Така поведінка пояснюється тим, що рядок "hello world"
сприймається як істинне значення.
Кожен об'єкт в JavaScript є truthly або falsy. Якщо об'єкт трапляється в булевому контексті, на зразок умовного виразу, він розглядається як true чи false залежно від його «істинності». Як саме визначається «істинність»? Є просте правило:
Усі значення є істинними, за винятком: false
, 0
, ""
(пустий рядок), null
, undefined
та NaN
.
Тож нехай вас не заплутує те, що "false"
, рядок "0"
, пустий об'єкт {}
та пустий масив []
є істинними значеннями. Ви можете переконатися в цьому, передавши об'єкт як аргумент функції Boolean (тобто Boolean("0")
).
Нам досить переконатися, що 0
— еквівалентно false.
Основа системи числення
0 1 2 3 4 5 6 7 8 9 10
Коли ми рахуємо від 0 до 9, то використовуємо різні символи для кожного числа (0–9). Коли ж ми доходимо до 10, нам вже потрібно два різні символи (1 та 0), щоб відтворити число. Усе тому, що основою десяткової системи числення є 10.
Основа — найменше число, яке може бути представлене більше ніж одним символом. Всі системи числення відрізняються основами, саме тому ті самі символи позначають різні цифри.
ДЕСЯТКОВА ДВІЙКОВА ШІСТНАДЦЯТКОВА
ОСНОВА=10 ОСНОВА=2 ОСНОВА=16
0 0 0
1 1 1
2 10 2
3 11 3
4 100 4
5 101 5
6 110 6
7 111 7
8 1000 8
9 1001 9
10 1010 A
11 1011 B
12 1100 C
13 1101 D
14 1110 E
15 1111 F
16 10000 10
17 10001 11
Для прикладу розглянемо наведену таблицю і побачимо, що символ 11
представляє різні числа в кожній системі числення. Для двійкової системи — це 3
, а якщо основа системи числення 16 — то 17
.
Ви могли помітити, що в нашому прикладі parseInt
повернув 3, коли аргументом було число 11. Тобто ми отримали число 3 у двійковому форматі, відповідно до наведеної таблиці.
Аргументи функцій
Функції в JavaScript можна викликати з будь-якою кількістю аргументів, навіть якщо ця кількість перевищує задану. Пропущені аргументи отримують значення undefined
, а зайві — ігноруються (проте зберігаються в масивоподібному об'єкті args
).
function foo(x, y) {
console.log(x);
console.log(y);
}
foo(1, 2); // logs 1, 2
foo(1); // logs 1, undefined
foo(1, 2, 3); // logs 1, 2
map
Ми вже майже досягли мети!
Map
— метод Array.prototype
, що повертає новий масив, кожен елемент якого обробляється визначеною функцією. Наприклад, цей фрагмент коду збільшує кожен елемент масиву в 3 рази:
function multiplyBy3(x) {
return x * 3;
}
const result = [1, 2, 3, 4, 5].map(multiplyBy3);
console.log(result); // logs [3, 6, 9, 12, 15];
Скажімо, я хочу вивести кожен елемент, використовуючи map()
(без виразу return
). Тож мені потрібно передати console.log
як аргумент, чи не так?
[1, 2, 3, 4, 5].map(console.log);
Проте відбувається щось дивне. Замість того щоб просто вивести одне значення, на виході отримуємо також індекс та повний масив.
[1, 2, 3, 4, 5].map(console.log);
// Код вище еквівалентний такому
[1, 2, 3, 4, 5].map(
(val, index, array) => console.log(val, index, array)
);
// але не такому
[1, 2, 3, 4, 5].map(
val => console.log(val)
);
Коли ми передаємо функцію в map()
, на кожній ітерації вона отримує три аргументи: currentValue
, currentIndex
та повний array
. Ось чому результат відрізняється від очікуваного.
У нас вже є все, щоб розгадати цю загадку.
Складаємо все докупи
ParseInt
приймає два аргументи: string
та radix
. Якщо значення radix
хибне, за замовчуванням встановлюється 10.
parseInt('11'); => 11
parseInt('11', 2); => 3
parseInt('11', 16); => 17
parseInt('11', undefined); => 11 (radix є falsy)
parseInt('11', 0); => 11 (radix є falsy)
Тепер проженемо наш приклад крок за кроком:
['1', '7', '11'].map(parseInt); => [1, NaN, 3]
// Перша ітерація: val = '1', index = 0, array = ['1', '7', '11']
parseInt('1', 0, ['1', '7', '11']); => 1
Оскільки 0 стосується хибних значень, основа системи числення встановлюється як 10. parseInt()
приймає лише два аргументи, тому масив ['1', '7', '11']
ігнорується. Символу 1 в десятковій системі числення відповідає число 1.
// Друга ітерація: val = '7', index = 1, array = ['1', '7', '11']
parseInt('7', 1, ['1', '7', '11']); => NaN
В системі числення з основою 1 числа 7
не існує. Як і в попередній ітерації, останній аргумент ігнорується. Саме тому parseInt()
повертає NaN
.
// Третя ітерація: val = '11', index = 2, array = ['1', '7', '11']
parseInt('11', 2, ['1', '7', '11']); => 3
У двійковій системі числення символ 11
відповідає числу 3
. Останній аргумент, як завжди, ігноруємо.
Підсумок (TLDR)
['1', '7', '11'].map(parseInt)
поводить себе неочікувано, тому що map
передає три аргументи в parseInt()
на кожній ітерації. Другий аргумент, index
, передається як параметр radix
. Тож кожен елемент масиву передається з різними значеннями основи системи числення, що відповідає індексу елемента: '7'
— з основою 1, тобто отримуємо на виході NaN
; '11'
— з основою 2, тому як результат отримуємо 3. '1'
використовує десяткову основу за замовчуванням, оскільки значення індексу 0 є хибним.
І наостанок — код, з яким отримаємо очікуваний результат:
['1', '7', '11'].map(numStr => parseInt(numStr));
Ще немає коментарів