17 June 2019

Почему ['1', '7', '11'].map(parseInt) возвращает [1, NaN, 3] в Javascript?

Entertaining tasksJavaScriptProgramming
Translation
Original author: Eric Tong


Javascript — странный. Не верите? Ну попробуйте тогда преобразовать массив строк в целые числа с помощью map и parseInt. Запустите консоль (F12 на Chrome), вставьте код ниже и нажмите Enter


['1', '7', '11'].map(parseInt);

Вместо ожидаемого массива целых чисел [1, 7, 11] мы получаем [1, NaN, 3]. Но как так? Чтобы узнать в чём тут дело, сначала нам придётся поговорить о некоторых базовых концепциях Javascript. Если вам нужен TL;DR, пролистывайте статью до самого конца.


Правдивость и ложность


Вот простой оператор if-else в Javascript:


if (true) {
    // всегда выполняется
} else {
    // не выполняется никогда
}

В этом случае условие оператора всегда истинно, поэтому блок if всегда выполняется, а блок else всегда игнорируется. Это тривиальный пример, потому что true — булев тип. Что тогда если мы поставим не булево условие?


if ("hello world") {
    // выполнится это?
    console.log("Условие истинно");
} else {
    // или это?
    console.log("Условие ложно");
}

Попробуйте запустить этот код в консоли разработчика. Вы должны увидеть «Условие истинно», так как строка «hello world» воспринимается как true.


Каждый объект в Javascript воспринимается либо как true, либо как false. При размещении в логическом контексте, таком как оператор if-else, объекты рассматриваются как true или false на основе их «истинности». Какие же объекты истинны, а какие ложны? Действует простое правило:


Все значения являются истинными, за исключением: false, 0, "" (пустая строка), null, undefined, и NaN.


Контр интуитивно это означает, что строка «false», строка «0», пустой объект {} и пустой массив [] — правдивы. Вы можете убедиться в этом самостоятельно, передав функции Boolean любой из объектов выше (например, Boolean(«0»);).


Но для наших целей просто достаточно помнить, что 0 это ложь.


Основание системы счисления


0 1 2 3 4 5 6 7 8 9 10

Когда мы считаем от нуля до девяти, мы используем разные символы для каждого из чисел (0-9). Однако, как только мы достигаем десяти, нам нужны два разных символа (1 и 0) для представления числа. Это связано с тем, что мы используем десятичную систему счисления.


Основание — наименьшее число, которое не представить только одним символом. У различных систем счисления разные основания, и поэтому, одни и те же цифры могут обозначать разные числа.


DECIMAL   BINARY    HEXADECIMAL
RADIX=10  RADIX=2   RADIX=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. Для шестнадцатеричной — это число 17.


Внимательный читатель вероятно заметил что код с parseInt возвращает 3, когда вход равен 11, что соответствует двоичному столбцу из таблицы выше.


Аргументы функции


Функции в Javascript можно вызывать с любым числом аргументов, даже если их количество в сигнатуре отлично. Отсутствующие параметры рассматриваются как неопределенные, а дополнительные просто игнорируются (но хранятся в похожем на массив объекте arguments object).


function foo(x, y) {
    console.log(x);
    console.log(y);
}

foo(1, 2);       // выводит 1, 2
foo(1);           // выводит 1, undefined
foo(1, 2, 3);   // выводит 1, 2

map()


Мы почти у цели!


Map — это метод в прототипе массива, который возвращает новый массив из результатов вызова функции для каждого элемента исходного массива. Например, следующий код умножает каждый элемент массива на 3:


function multiplyBy3(x) {
    return x * 3;
}

const result = [1, 2, 3, 4, 5].map(multiplyBy3);

console.log(result);   // выводит [3, 6, 9, 12, 15];

Теперь предположим, что я хочу вывести каждый элемент используя map() (и не используя return). Можно просто передать console.log в качестве аргумента в map() … правильно?


[1, 2, 3, 4, 5].map(console.log);


Происходит что-то странное. Вместо того чтобы выводить только значение, каждый вызов 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 ложен)
parseInt('11', 0);              => 11 (radix ложен)

Давайте рассмотрим этот пример шаг за шагом.


['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' по основанию 10 даст результат 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. Последний аргумент вновь игнорируется.


Итог (TL;DR)


['1', '7', '11'].map(parseInt) не работает как было задумано, потому что map передает три аргумента в parseInt() на каждой итерации. Второй аргумент index передается в parseInt в качестве параметра radix (основание системы счисления). Таким образом, каждая строка в массиве анализируется с использованием недефолтного основания. '7' анализируется по основанию 1, что даёт NaN; '11' анализируется как двоичное число — итог 3. '1' анализируется по дефолтному основанию 10, потому что его индекс 0 является ложным.


А вот код, который будет работать так, как мы хотели:


['1', '7', '11'].map(numStr => parseInt(numStr));


Tags: map parseInt
Hubs: Entertaining tasks JavaScript Programming
+103
57.1k 134
Comments 145
Ads
Top of the day