Medium의 Daily JS, Eric Tong님의 글 Why ['1''7''11'].map(parseInt) returns [1, NaN, 3] in Javascript입니다.


JavaScript는 이상합니다. 저(Eric Tong)를 믿지 않습니까? Map과 parseInt를 사용해 문자 배열을 정수들로 바꾸기를 시도해보세요. Console을 시작(Chrome에서 F12)하고 다음을 붙여넣으신 다음 엔터를 누르세요(또는 pen에서 실행하세요).

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

정수 배열(['1, 7, 11])을 돌려주는 대신 [1, NaN, 3]으로 끝날 것입니다. 뭐? 어떻게 돌아가는지 알기 위해서 먼저 JavaScript의 콘셉에 대해서 이야기해야 합니다. 만약 TLDR을 좋아하시는 분을 위해 제가 이야기 끝에 요약본을 포함했습니다.

진실과 거짓

여기 간단한 JavaScript if-else 구문이 있습니다:

if (true) {
  // 이 구문은 항상 실행됩니다
} else {
  // 절대로 실행되지 않습니다
}

이 경우에 if-else 구문이 참이므로 if 부분이 항상 실행되고 else 부분은 항상 무시됩니다. 이것은 true가 boolean이기 때문에 간단합니다. 우리가 boolean이 아닌 것들을 넣으면 어떻게 될까요?

if ("hello world") {
  // 이게 실행될까요?
  console.log("Condition is trusty");
else {
  // 아니면 이거?
  console.log("Condition is falsy");
}

개발자 도구 콘솔(Chrome에서 F12)에서 코드를 실행해보세요. 당신은 if 부분이 실행되는 것을 확인할 수 있습니다. 이것은 문자열 "hello world"가 참(truthy)이기 때문입니다.

모든 JavaScript 객체는 참(truthy) 또는 거짓(falsy)입니다. if-else 구문과 같은 곳에 boolean을 집어넣으면 그것의 진실성에 의해 객체는 참 또는 거짓으로 대해집니다. 그래서 어떤 객체가 참이며 어떤 객체가 거짓일까요? 여기 따를 간단한 규칙이 있습니다:

다음 값들을 제외한 모든 값은 참으로 대해집니다: false, 0, ”(빈 문자열), null, undefined 그리고 NaN

헷갈리게도 이것은 문자열 false, 문자열 0, 빈 객체 {} 그리고 빈 배열 []이 참이라는 것을 의미합니다. 당신은 Boolean 함수에 객체를 통과(예시: Boolean("0");)시켜 이것을 다시 확인할 수 있습니다.

우리 주제를 위해서 0도 거짓이라는 것을 기억해야 합니다.

밑(Radix)

0 1 2 3 4 5 6 7 8 9 10

우리가 0에서 9까지 숫자를 셀 때, 우리는 각 숫자에 대해 다른 기호(0-9)를 사용합니다. 그러나 우리가 10에 다다르면 숫자를 표현하기 위해 우리는 2개의 다른 기호(1과 0)를 사용합니다. 이것은 우리의 10진법 구조가 밑이 10이기 때문입니다.

밑은 하나 이상의 기호로 나타낼 수 있는 가장 작은 수입니다. 다른 수 체계(진법 체계)는 다른 밑을 가지고 있고 그래서 같은 수들이 수 체계에서 다른 수를 나타낼 수 있게 됩니다.

DECIMAL   BINARY    HEXADECIMAL
RADIX=10  RADIX=2   RADIX=16

0         0         0
1         1         1
2         102
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의 입력이 11일 때 출력이 3임이 위 표에서 바이너리 행에 해당함을 알아챘어야 합니다.

함수의 매개변수

JavaScript에서 함수는 정의된 함수의 매개변수의 수나 호출하는 매개변수의 수에 제한받지 않고 호출될 수 있습니다. 빠진 매개변수는 undefined로 대해지며 초과된 매개변수들은 무시됩니다(하지만 배열로 매개변수 객체에 저장됩니다).

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

이제 저희는 거의 결론에 도달했습니다!

Map은 원래 배열의 각 원소를 함수로 전달한 결과를 새 배열로 반환하는 Array 프로토타입의 메소드입니다. 예를 들어 다음 코드는 각 원소에 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()을 사용하여 모든 원소를 출력(아무것도 돌려주는 값 없이)하고 싶습니다. 저는 map()을 통해 console.log를 통과시킬 수 있습니다… 맞죠?

[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()을 통과했을 때, 각각의 때마다 3개의 매개변수가 함수를 통과했습니다: 현재 값, 현재 인덱스, 그리고 전체 배열입니다. 이것이 왜 때마다 3개가 출력되었는지 입니다.

이제 저희는 모든 조각을 가지고 있고 미스터리를 풀 차례입니다.

모두 한 곳에 가져오세요

ParseInt는 2가지 매개변수를 사용합니다: 문자열입니다. 만약 밑이 제공되지 않았다면 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()는 오직 2개의 매개변수만 가져가기 때문에 세 번째 매개변수 ['1', '7', '11']은 무시되었습니다. 10진법에서 문자열 '1'은 숫자 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을 가리킵니다. 마지막 매개변수는 무시되었습니다.

요약 (TLDR)

['1', '7', '11'].map(parseInt)는 각 때마다 3개의 매개변수를 전달해 의도한대로 작동하지 않습니다. 두 번째 매개변수인 indexradix로서 parseInt를 통과했었습니다. 그래서 배열의 각 문자열이 각각 다른 진법을 사용해 해석되었습니다. '7'은 1진법으로 해석되어 NaN으로, '11'은 2진법으로 해석되어 3이 되었습니다. '1'은 인덱스 0이 거짓이므로 기본값의 진법인 10진법으로 해석되었습니다.

그래서 다음 코드가 의도한 대로 작동할 것입니다:

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