WTFJS는 자바스크립트의 재밌는(?)면들을 소개하고 있는 유명한 깃헙 repo중 하나입니다.
오랜만에 자바스크립트를 다시 한 번 정리해볼겸, 44d019116162b00c2093cae9f4ac4ef18b5d002f커밋을 기준으로 소개되어 있는 섹션들에 대한 해설을 작성해보고자 합니다.
대부분 설명이 나와있긴 하지만, ECMA-262 문서를 참고해야 하는 섹션들이 꽤 있어서, 이에 대한 해설을 작성하였습니다.
또한, WTFJS에 있는 설명 내용을 그대로 직역하기보다 제가 알고있는 지식을 동원하여 나름대로 해석해서 설명해놓았으므로, 틀린 내용이나 용어가 있을 수 있으므로 이를 참고하여 주시기 바라며, 피드백을 주시면 정말 감사하겠습니다.
// ->
는 표현식(expression)의 결과를 나타냅니다.
1 + 1; // -> 2
// >
는 console.log
의 결과 등과 같은 출력값을 나타냅니다.
console.log('hello, world!'); // > hello, world!
//
는 순수한 코멘트를 나타냅니다.
// foo const에 함수를 할당함
const foo = function() {};
[]
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 []
tootrue == []; // -> false
true == ![]; // -> false
false == []; // -> true
false == ![]; // -> true
이 내용은 바로 윗 섹션의 연장선입니다. ![]
가 false
로 변환됨을 생각해보면 true == ![]
와 false == ![]
의 결과를 쉽게 이해할 수 있습니다.
true == []
, false == []
의 경우 각각 숫자로 형변환이 될텐데, true
는 1
로, false
와 빈 배열인 []
는 0
으로 변환되므로 이를 대입해보면 그 결과가 이해될 것입니다.
!!'false' == !!'true'; // -> true
!!'false' === !!'true'; // -> true
!
연산자의 결과는 boolean
형임을 생각해보면 쉽게 이해할 수 있습니다. 빈 문자열(''
)이 아닌 문자열은 truthy
로 평가됩니다. 따라서, !'false'
!'true'
는 모두 false
가 됩니다. 이를 다시 !
로 부정해버리면 true
가 됩니다. 따라서 true == true
, true === true
가 되므로 true
가 됩니다.
'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
라니!
이는 명세를 보면 아주 쉽게 이해가 되는데,
즉, ===
연산자를 사용했을 때 첫번째 값의 타입이 Number
인 경우, 둘 중 하나라도 NaN
이면 무조건 false
가 나오게 됩니다.
이러한 내용을 알고 있는 것은 꽤 중요한데, 예를 들어 어떤 변수의 값이 NaN
인지 판단할 때 이렇게 ===
연산자로 판단하면 안 된다는 것입니다.
let expectNumber = x; // 숫자를 기대하는 값
// .. 어떤 코드들...
if (expectNumber === NaN) {
// 이 브런치는 절대 도달하지 않음!
} else {
// 이 브런치만 항상 실행!
}
위와 같이 예상하기 힘든 버그를 유발할 수 있기 때문입니다. 어떤 값이 NaN
인지 확인하고자 한다면 Number.isNaN
을 사용하여 확인해야 합니다.
Object.is()
and ==
weird casesObject.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
(![] + [])[+[]] +
(![] + [])[+!+[]] +
([![]] + [][[]])[+!+[] + [+[]]] +
(![] + [])[!+[] + !+[]];
// -> 'fail'
자바스크립트에서 재밌는 점 중 하나라고 생각하는 예제입니다. 핵심은,
- 특정 타입의 값을 문자열로 변환
- 변환된 문자열을
[]
를 이용하여 인덱스 접근하여 문자열 결합
이것이 핵심이라고 할 수 있습니다.
우선, 당장 눈에 잘 띄는 것 중 하나는 (![] + [])
입니다. 얘를 먼저 해석해봅시다.
![]
는 위에서 이미 언급했듯, 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
이 되고, !0
은 falsy
값인 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'
이 됩니다.
WTFJS의 섹션이 워낙 많다보니, 이를 하나의 포스팅으로 정리하게 되면 너무 지칠것 같아 나눠서 정리해보도록 하겠습니다.
틀린점이 있으면 피드백 부탁드리며, 다음 시간에 뵙겠습니다.