WTFJS 해설 - 1

undefcat·2021년 4월 13일
0

WTFJS

목록 보기
1/8
post-thumbnail

WTFJS

WTFJS는 자바스크립트의 재밌는(?)면들을 소개하고 있는 유명한 깃헙 repo중 하나입니다.

오랜만에 자바스크립트를 다시 한 번 정리해볼겸, 44d019116162b00c2093cae9f4ac4ef18b5d002f커밋을 기준으로 소개되어 있는 섹션들에 대한 해설을 작성해보고자 합니다.

대부분 설명이 나와있긴 하지만, ECMA-262 문서를 참고해야 하는 섹션들이 꽤 있어서, 이에 대한 해설을 작성하였습니다.

또한, WTFJS에 있는 설명 내용을 그대로 직역하기보다 제가 알고있는 지식을 동원하여 나름대로 해석해서 설명해놓았으므로, 틀린 내용이나 용어가 있을 수 있으므로 이를 참고하여 주시기 바라며, 피드백을 주시면 정말 감사하겠습니다.

Notation

// ->는 표현식(expression)의 결과를 나타냅니다.

1 + 1; // -> 2

// >console.log 의 결과 등과 같은 출력값을 나타냅니다.

console.log('hello, world!'); // > hello, world!

//는 순수한 코멘트를 나타냅니다.

// foo const에 함수를 할당함
const foo = function() {};

Examples

[] is equal ![]

[] == ![]; // -> true

💡 해설

자바스크립트는 엄격한 타입 언어가 아니기 때문에 타입을 비교하지 않는 == 연산자를 사용하는 경우, 비교 대상 둘의 타입이 같지 않으면 내부 구현 규칙에 따라 비교연산이 수행되어 의도치 않은 결과가 나올 수 있습니다.

따라서 보통 == 비교 연산자보다는 === 비교 연산자를 사용하기를 권장하는 경우가 많습니다. 저 또한 되도록이면 ===를 사용하려고 노력하고 있으며, ==를 사용하는 경우 의도적으로 사용했다고 주석을 남기곤 합니다.

WTFJS의 많은 섹션들은 타입이 같지 않은 두 피연산자들에 관한 내용이 주를 이룹니다.

아무튼, 위의 예제 코드에서 [] == ![]를 한 번 살펴봅시다.

![]는 배열을 부정하고 있습니다. 사실 !boolean 형의 값에 붙이는 것이 일반적인데, 자바스크립트에서는 boolean 형으로 해석해야 되는 컨텍스트(조건문 등)에서 boolean이 아닌 다른 타입의 값이 사용되는 경우, 규칙에 따라 true, false로 평가됩니다. 이를 보통 truthy falsy로 표현합니다.

여기서 중요한 것은 단지 평가가 되는 것일 뿐, boolean형으로 형변환이 되는 것이 아니라는 점입니다.

그래서 다시 ![]를 살펴보면 배열을 부정하고 있는데, ! 연산자의 결과는 boolean이기 때문에 이를 위해 자바스크립트는 []가 우선 truthy한지 falsy한지 판단합니다.

자바스크립트에서 빈 배열인 []truthy한 값입니다. 자바스크립트에서 일반적인 객체는 모두 truthy하게 취급됩니다.

보통 처음 jQuery를 배우시는 분들이 실수하는 경우 중 하나가 바로 아래와 같은 코드입니다.

if ($('.section')) {
  // .section이 있는 경우 실행되길 기대하는 코드
} else {
  // .section이 없는 경우 실행되길 기대하는 코드
}

단순하게 생각해보면, $('.section').section 엘리먼트들을 모두 선택하라는 것인데, 없는 경우 값이 선택되지 않았을 것이므로 else문을 탈 것이라고 생각하는 경우가 있습니다. 하지만 $()의 경우, 이는 jQuery 객체이고, 객체는 truthy하게 평가되기 때문에 else문을 타는 경우는 없습니다. jQuery에서 어떤 요소가 없는지를 판단하려면

if ($('.section').length) {
  // ...
} else {
  // ...
}

위와 같이 length 프로퍼티를 이용하여, jQuery 객체가 담고 있는 요소들의 갯수를 이용하여 코드를 작성해야 합니다. 선택된 것이 없으면 0이고, 0은 자바스크립트에서 falsy한 값으로 판단되므로 기대한대로 작동합니다.

다시 돌아와서, == 연산자는 우선 피연산자들의 형을 파악하고 형이 다르면 규칙에 따라 형변환을 시도합니다.

[] == ![] 에서 ! 연산자의 우선 순위가 더 높으므로, [] == false가 됩니다. 두 개의 형이 다르므로, 규칙에 따라 형변환을 시도합니다.

형변환의 상세규칙은 Abstract Equality Comparison을 참고해보시면 되겠지만, 일반적으로 숫자로 변환 된다고 생각하면 됩니다.

[] == false의 피연산자들은 각각 숫자형으로 변환되는데, 빈 배열인 []는 자바스크립트에서 0이고, false 역시 0입니다.

결과적으로 0 == 0을 비교하게 되어, true가 됩니다.

true is not equal ![], but not equal [] too

true == []; // -> false
true == ![]; // -> false

false == []; // -> true
false == ![]; // -> true

💡 해설

이 내용은 바로 윗 섹션의 연장선입니다. ![]false로 변환됨을 생각해보면 true == ![]false == ![]의 결과를 쉽게 이해할 수 있습니다.

true == [], false == []의 경우 각각 숫자로 형변환이 될텐데, true1로, false와 빈 배열인 []0으로 변환되므로 이를 대입해보면 그 결과가 이해될 것입니다.

true is false

!!'false' == !!'true'; // -> true
!!'false' === !!'true'; // -> true

💡 해설

! 연산자의 결과는 boolean형임을 생각해보면 쉽게 이해할 수 있습니다. 빈 문자열('')이 아닌 문자열은 truthy로 평가됩니다. 따라서, !'false' !'true'는 모두 false가 됩니다. 이를 다시 !로 부정해버리면 true가 됩니다. 따라서 true == true, true === true가 되므로 true가 됩니다.

baNaNa

'b' + 'a' + +'a' + 'a'; // -> 'baNaNa'

💡 해설

중간에 'a' + +'a'를 이해하면 됩니다. 'a'+ +'a'의 경우, 뒷 부분의 +'a'는 숫자로 해석됩니다. 자바스크립트에서 + unary operator는 해당 값을 숫자형으로 변환시키는데, 문자열 'a'는 숫자로 변환되지 않으므로 NaN이 됩니다. 그러면 'a' + NaN이 되는데, 문자열이 NaN 앞에 있으므로 +는 문자열 결합연산자로 동작하면서 피연산자를 문자열로 해석하게 됩니다.

자바스크립트에서 어떤 데이터가 문자열로 해석되어야 하는 경우 toString 메서드를 호출합니다. NaN.toString()의 결과는 'NaN'이므로, 결과적으로 'baNaNa'가 됩니다.

NaN is not a NaN

NaN === NaN; // -> false

💡 해설

제가 자바스크립트를 처음 공부했을 때, 코뿔소책에서 위의 내용을 본 적 있는데 아직까지 잊혀지지가 않습니다. 같은 값을 비교했는데, 그 값이 false라니!

이는 명세를 보면 아주 쉽게 이해가 되는데,

  1. If Type(x) is different from Type(y), return false.
  2. If Type(x) is Number, then
    1. If x is NaN, return false.
    2. If y is NaN, return false.
    … … …

즉, === 연산자를 사용했을 때 첫번째 값의 타입이 Number인 경우, 둘 중 하나라도 NaN이면 무조건 false가 나오게 됩니다.

이러한 내용을 알고 있는 것은 꽤 중요한데, 예를 들어 어떤 변수의 값이 NaN인지 판단할 때 이렇게 === 연산자로 판단하면 안 된다는 것입니다.

let expectNumber = x; // 숫자를 기대하는 값

// .. 어떤 코드들...

if (expectNumber === NaN) {
  // 이 브런치는 절대 도달하지 않음!
} else {
  // 이 브런치만 항상 실행!
}

위와 같이 예상하기 힘든 버그를 유발할 수 있기 때문입니다. 어떤 값이 NaN인지 확인하고자 한다면 Number.isNaN을 사용하여 확인해야 합니다.

Object.is() and == weird cases

Object.is(NaN, NaN); // -> true
NaN === NaN; // -> false

Object.is(-0, 0); // -> false
-0 === 0; // -> true

Object.is(NaN, 0 / 0); // -> true
NaN === 0 / 0; // -> false

💡 해설

대부분의 경우, Object.is로 비교를 하게 되면 직관적인 기대대로 동작하게 됩니다. 이 코드에서 눈여겨 볼 것 중 하나는 자바스크립트에서는 0/0와 같이 0으로 나눠버리는 경우, 많은 언어에서는 이를 예외(Divide by zero)로 내뿜지만 자바스크립트에서는 이를 특별히 처리한다는 점을 기억하면 될 것 같습니다.

  • 0/0: NaN
  • 양수/0: Infinity
  • 음수/0: -Infinity

It's a fail

(![] + [])[+[]] +
  (![] + [])[+!+[]] +
  ([![]] + [][[]])[+!+[] + [+[]]] +
  (![] + [])[!+[] + !+[]];
// -> 'fail'

💡 해설

자바스크립트에서 재밌는 점 중 하나라고 생각하는 예제입니다. 핵심은,

  1. 특정 타입의 값을 문자열로 변환
  2. 변환된 문자열을 []를 이용하여 인덱스 접근하여 문자열 결합

이것이 핵심이라고 할 수 있습니다.

우선, 당장 눈에 잘 띄는 것 중 하나는 (![] + []) 입니다. 얘를 먼저 해석해봅시다.

![]는 위에서 이미 언급했듯, false 입니다. 그렇다면 false + []는 무엇일까요? 자바스크립트에서는 숫자가 아닌 타입에 + - * / 등의 숫자 연산을 시도하면(물론, +의 경우 피연산자가 문자열인 경우 문자열 결합연산자로 동작하여 문자열로 변환을 시도합니다) valueOf 메서드를 호출합니다.

예를 들어

const obj = {
  valueOf() {
    return 10;
  }
};

obj + 20; // -> 30

위와 같이 됩니다. 일반적인 객체의 경우 valueOf의 값은 특별히 정의되어 있지 않으면 Object.prototype.valueOf가 호출되는데, 이 값은 this, 즉 자기 자신입니다. 이렇게 valueOf를 호출했는데 그 결과가 숫자가 여전히 아니라면, toString을 호출합니다. 즉, 문자열 변환을 시도합니다.

따라서, false + []false.valueOf()를 호출해도 여전히 자기자신인 false가 되므로, 이를 false.toString()으로 문자열 변환을 시도하며 결과는 'false' 인 문자열입니다. 배열의 경우, toString이 호출되면 .join(',')과 같이 호출됩니다. 따라서, 빈 배열인 []의 문자열 변환 결과는 빈 문자열인 ''가 됩니다. 결국, 'false' + ''가 되어 문자열 'false'가 됩니다.

따라서 (![] + [])[+[]]는 우선 ('false')[+[]]가 됩니다. 뒤의 [+[]]의 경우, 우선 +[]가 해석될 것입니다. +는 숫자로 변환이 된다고 하였고, 빈 문자열인 []0이므로 최종적으로 ('false')[0]이 됩니다. 이는 'false'[0]가 되므로, 'false' 문자열의 0번째 인덱스값인 'f'가 됩니다.

두번째 줄인 (![] + [])[+!+[]]의 경우, 앞부분은 위에서 알아보았듯 'false'일 것이고, 뒷부분의 경우 +!+[]를 해석하면 됩니다. +!+[]의 경우, 연산자 우선순위에 의해 +[]0이 되고, !0falsy값인 0을 부정하였으므로 true가 됩니다. 결국 +true가 되므로, true를 숫자로 형변환하게되면 1이 됩니다. 따라서 'false'[1]이 되고 결과적으로 문자열 'a'가 됩니다.

세번째 줄인 ([![]] + [][[]])[+!+[] + [+[]]]를 해석해봅시다.

우선 앞부분인 ([![]] + [][[]])를 해석해보겠습니다.

  • ![]false 입니다.
  • [false] + [][[]]가 됩니다.
  • 숫자가 아닌 두 타입이 +로 계산되므로, 앞부분은 결국 [false].toString()이 호출될 것이고, 이는 배열의 0번째 값을 문자열로 출력한 'false'가 됩니다.
  • 뒤의 [][[]]의 경우, 빈 배열 [][[]] 인덱스를 뜻합니다.
  • [[]]의 경우, []번째 인덱스이므로 []가 숫자로 해석되는데, []0이 될 것이고, 결국 [][0]인데, 아무 것도 없으므로 undefined가 됩니다.
  • 따라서 'false'+undefined 인데, undefined가 문자열로 결합되므로 'falseundefined'가 됩니다.

이제 'falseundefined'[+!+[] + [+[]]]를 해석해보겠습니다.

  • +!+[]은 앞에서도 했듯이, 1입니다.
  • [+[]]의 경우, [0] 입니다.
  • 1 + [0]이므로 숫자와 다른 타입간의 계산이므로 결국 [0].toString이 호출될 것이고, 이 결과는 문자열 '0'이고, 문자열간에 +를 계산하므로 결국 문자열 '10'이 됩니다.
  • 최종적으로 'falseundefined'['10'] 이므로, 'falseundefined'10번째 인덱스값은 i 입니다.

마지막 부분인 (![] + [])[!+[] + !+[]]의 설명은 생략하겠습니다. 결국 최종적으로는 'f' + 'a' + 'i' + 'l'이 되어 'fail'이 됩니다.

1편을 마치며

WTFJS의 섹션이 워낙 많다보니, 이를 하나의 포스팅으로 정리하게 되면 너무 지칠것 같아 나눠서 정리해보도록 하겠습니다.

틀린점이 있으면 피드백 부탁드리며, 다음 시간에 뵙겠습니다.

profile
undefined cat

0개의 댓글