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

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.


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));


Источник: https://habr.com/ru/post/456344/


Интересные статьи

Интересные статьи

Недавно на Хабре была опубликована статья Главная причина, почему не Linux, которая наделала много шума в обсуждениях. Данная заметка — это небольшой философский ответ на ту статью, который, как ...
Привет, Хабр! Представляю вашему вниманию перевод статьи «f5 Reasons AI Won’t Replace Humans… It Will Make Us Superhuman». Многие говорят, что ИИ с немыслимой скоростью забирает у нас работу. ...
Битрикс24 — популярная в малом бизнесе CRM c большими возможностями даже на бесплатном тарифе. Благодаря API Битрикс24 (даже в облачной редакции) можно легко интегрировать с другими системами.
Перфекционизм — ласковый убийца. Он убил больше нервов, отношений и проектов, чем кухонный нож, автомат Калашникова и твой заказчик вместе взятые. В этой статье я объясню, почему тебе не нужно...
В последнее время появилось много фантастических исследований по 2D-рендерингу. Пётр Кобаличек и Фабиан Айзерман работают над Blend2D: это один из самых быстрых и точных CPU-растеризаторов на рын...