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

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

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

Истина и ложь

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

if (true) {
    // this always runs
} else {
    // this never runs
}

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

if ("hello world") {
    // will this run?
    console.log("Condition is truthy");
} else {
    // or this?
    console.log("Condition is falsy");
}

Попробуйте запустить этот код в консоли разработчика (F12 в Chrome). Вы должны обнаружить, что блок if работает. Это потому, что строковый объект "hello world" правдив.

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

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

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

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

Radix

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 могут означать разные числа в разных системах счета. Если основание системы счисления 2, то это относится к числу 3. Если основание системы счисления 16, то оно относится к числу 17.

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

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

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

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

карта()

Мы почти там!

Карта - это метод в прототипе массива, который возвращает новый массив результатов передачи каждого элемента исходного массива в функцию. Например, следующий код умножает каждый элемент в массиве на 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() (без операторов возврата). Я должен иметь возможность передать console.log в качестве аргумента map()… верно?

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

Происходит что-то очень странное. Вместо того, чтобы регистрировать только значение, каждый console.log вызов также регистрировал индекс и полный массив.

[1, 2, 3, 4, 5].map(console.log);
// The above is equivalent to
[1, 2, 3, 4, 5].map(
    (val, index, array) => console.log(val, index, array)
);
// and not equivalent to
[1, 2, 3, 4, 5].map(
    val => console.log(val)
);

Когда функция передается в map(), для каждой итерации в функцию передаются три аргумента: currentValue, currentIndex и полный array. Вот почему для каждой итерации регистрируется три записи.

Теперь у нас есть все необходимое, чтобы разгадать эту загадку.

Собираем все вместе

ParseInt принимает два аргумента: string и radix. Если предоставленное основание системы счисления ложное, то по умолчанию установлено значение 10.

parseInt('11');                => 11
parseInt('11', 2);             => 3
parseInt('11', 16);            => 17
parseInt('11', undefined);     => 11 (radix is falsy)
parseInt('11', 0);             => 11 (radix is falsy)

Теперь давайте рассмотрим наш пример по шагам.

['1', '7', '11'].map(parseInt);       => [1, NaN, 3]
// First iteration: val = '1', index = 0, array = ['1', '7', '11']
parseInt('1', 0, ['1', '7', '11']);   => 1

Поскольку 0 является ложным, для системы счисления устанавливается значение по умолчанию 10. parseInt() принимает только два аргумента, поэтому третий аргумент ['1', '7', '11'] игнорируется. Строка '1' в системе счисления 10 относится к числу 1.

// Second iteration: val = '7', index = 1, array = ['1', '7', '11']
parseInt('7', 1, ['1', '7', '11']);   => NaN

В системе счисления 1 символ '7' не существует. Как и в первой итерации, последний аргумент игнорируется. Итак, parseInt() возвращает NaN.

// Third iteration: val = '11', index = 2, array = ['1', '7', '11']
parseInt('11', 2, ['1', '7', '11']);   => 3

В двоичной системе счисления 2 символ '11' относится к числу 3. Последний аргумент игнорируется.

Резюме (TL; DR)

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

Итак, следующий код будет работать по назначению:

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

Понравилась эта история? Вот история того, как я начал заниматься фронтенд-разработкой.