Странный 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));
Понравилась эта история? Вот история того, как я начал заниматься фронтенд-разработкой.