Dlaczego ['1', '7', '11'].map(parseInt) zwraca [1, NaN, 3] w JS
Javascript jest dziwny. Nie wierzysz mi? Spróbuj przekonwertować tablicę łańcuchów na liczby całkowite za pomocą map i parseInt. Uruchom konsolę (F12 w Chrome), wklej kod poniżej i naciśnij Enter.
['1', '7', '11'].map(parseInt);
Zamiast podawać nam tablicę liczb całkowitych [1, 7, 11]
, dostajemy [1, NaN, 3]
. Lol? Aby dowiedzieć się, co tu się u licha dzieje, musimy najpierw porozmawiać o kilku koncepcjach JavaScriptu. Jeśli chcesz wersję TL;DR, zamieściłem krótkie streszczenie pod koniec artykułu.
“Prawdziwość” i “fałszywość”
Oto prosta instrukcja if-else w Javascript:
if (true) {
// this always runs
} else {
// this never runs
}
W tym przypadku warunek instrukcji if-else jest prawdziwy, więc blok if jest zawsze wykonywany, a blok else jest ignorowany. Jest to trywialny przykład, ponieważ true jest booleanem. A co, jeśli jako warunek postawimy nieboolana?
if ("hello world") {
// will this run?
console.log("Condition is truthy");
} else {
// or this?
console.log("Condition is falsy");
}
Spróbuj uruchomić ten kod w konsoli (F12 na Chrome). Blok if powinien się wykonać. Dzieje się tak dlatego, że obiekt łańcucha znaków "hello world"
jest prawdą.
Każdy obiekt JavaScript jest albo “prawdziwy” albo “fałszywy”. W kontekście boolean, takim jak instrukcja "if-else", obiekty są traktowane jako prawdziwe lub fałszywe w oparciu o ich prawdziwość. Więc które obiekty są prawdziwe, a które fałszywe? Oto prosta zasada, której należy przestrzegać:
Wszystkie wartości są prawdziwe, z wyjątkiem: false, 0, "" (pusty łańcuch), null,undefined i NaN.
Mylnie, oznacza to, że łańcuch "false"
, łańcuch "0"
, pusty obiekt {}
, i pusta tablica []
są wszystkie prawdziwe. Można to potwierdzić, przekazując obiekt do funkcji boolean (np. Boolean("0");
).
W naszym wypadku, wystarczy jedynie pamiętać, że 0
jest fałszywe.
Podstawa (Radix)
0 1 2 3 4 5 6 7 8 9 10
Licząc od zera do dziewięciu, posiadamy różne symbole dla każdej z liczb (0-9). Jednak gdy osiągniemy dziesięć, musimy użyć dwóch różnych symboli (1 i 0), aby reprezentować liczbę. Dzieje się tak dlatego, że nasz system liczenia dziesiętnego ma podstawę równą dziesięć.
Podstawa jest najmniejszą liczbą, która może być reprezentowana przez więcej niż jeden symbol. Różne systemy liczbowe mają różne podstawy, a więc te same cyfry mogą odnosić się do różnych liczb w systemach liczbowych.
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
Na przykład, patrząc na powyższą tabelę, widzimy, że te same cyfry 11 mogą oznaczać różne liczby w różnych systemach liczenia. Jeśli podstawa wynosi 2, to odnosi się do liczby 3. Jeśli podstawa wynosi 16, to odnosi się do liczby 17.
Być może zauważyłeś, że w naszym przykładzie, parseInt zwrócił 3, gdy wejście jest 11, co odpowiada kolumnie binarnej w tabeli powyżej.
Argumenty funkcji
Funkcje w Javascript mogą być wywoływane z dowolną liczbą argumentów, nawet jeśli nie są one równe liczbie zadeklarowanych parametrów funkcji. Brakujące argumenty są traktowane jako niezdefiniowane, a dodatkowe argumenty są ignorowane (ale są przechowywane w obiekcie argumentów przypominającym tablicę).
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()
Już prawie jesteśmy na miejscu!
Map jest metodą w prototypie tablicy, która zwraca nową tablicę wyników powstałą w wyniku przekazania każdego elementu oryginalnej tablicy do funkcji. Na przykład, następujący kod mnoży każdy element tablicy przez 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];
Teraz, powiedzmy, że chcę wypisać każdy element za pomocą map()
(bez zwracania wartości). Powinienem być w stanie przekazać console.log
jako argument do map()
... prawda?
[1, 2, 3, 4, 5].map(console.log);
Coś bardzo dziwnego się dzieje. Zamiast wypisywać samą wartość, każdy console.log
wypisze również indeks i pełną tablicę.
[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)
);
Gdy funkcja jest przekazywana do map()
, dla każdej iteracji, do funkcji przekazywane są trzy argumenty: currentValue
, currentIndex
i pełna tablica array
. Dlatego też w każdej iteracji wypisywane są 3 wartości.
Mamy teraz wszystkie dowody potrzebne do rozwiązania tej tajemnicy.
Zbierzmy wszystko do kupy
ParseInt przyjmuje dwa argumenty: string
i radix
. Jeśli podany radix jest błędny, to domyślnie ustawiany jest na 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)
Teraz przejdźmy przez nasz przykład krok po kroku.
['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
Ponieważ 0 jest fałszywe, podstawa zostaje ustawiona na domyślną wartość 10. parseInt()
przyjmuje tylko dwa argumenty, więc trzeci argument ['1'', '7', '11']
jest ignorowany. Ciąg "1"
w podstawie 10 odnosi się do liczby 1.
// Second iteration: val = '7', index = 1, array = ['1', '7', '11']
parseInt('7', 1, ['1', '7', '11']); => NaN
W systemie liczbowym o podstawie 1 symbol "7"
nie istnieje. Podobnie jak w przypadku pierwszej iteracji, ostatni argument jest ignorowany. Więc, parseInt()
zwraca NaN
.
// Third iteration: val = '11', index = 2, array = ['1', '7', '11']
parseInt('11', 2, ['1', '7', '11']); => 3
W systemie o podstawie 2 (binarnym) symbol "11"
odnosi się do liczby 3. Ostatni argument jest ignorowany.
Podsumowanie (TL;DR)
['1'', '7', '11'].map(parseInt)
nie działa zgodnie z przewidywaniami, ponieważ map
przekazuje przez trzy argumenty do parseInt()
na każdej iteracji. Drugi argument index jest przekazywany do parseInt jako parametr radix
. Tak więc każdy łańcuch w tablicy jest przetwarzany przy użyciu innej podstawy. 7"
jest przetwarzany jako radix 1, czyli NaN
, "11"
jest przetwarzany jako radix 2, czyli 3. "1"
jest przetwarzany jako domyślny radix 10, ponieważ jego indeks 0 jest błędny.
Tak więc, następujący kod będzie działał zgodnie z przeznaczeniem:
['1', '7', '11'].map(numStr => parseInt(numStr));
Oryginał tekstu w języku angielskim przeczytasz tutaj.