First written: 23-01-21
Uploaded: 23-03-31
Last modified: 23-04-01
+
operation은 어느 한 쪽의 피연산자가 string이면(혹은 객체와 같이 모종의 과정을 통해 string이 된다면) string을 결합하는 연산을 수행한다.
[1, 2] + [3, 4]; // "1, 23, 4"
6 + "6"; // "66"
{} + []; // [object Object]
[] + {}; // [object Object]
그러므로 이를 활용해 number를 string으로 바꾸는 아래와 같은 방법은 자주 사용되는 방법이기도 하다.
26 + ""; // "26"
다만 명시적 강제변환의 경우와 비교해 유의할 점이 있다. String(a)
는 value a를 toString()
메서드 호출을 통해 바꿔주지만, a + ""
는 valueOf()
메서드를 통해 string으로 변환한다. 따라서, valueOf()
나 toString()
을 직접 정의한 경우에는 각각의 연산 결과가 달라질 수 있으니 이 점에 유의해야할 것이다.
Unary operator를 활용해 boolean value를 number value로 바꿔주는 트릭을 사용하면 좋은 순간도 간혹 있다. 아래의 코드를 보자.
function onlyOneTrue(a, b, c) {
return !!((a && !b && !c) || (!a && b && !c) || (!a && !b && c));
}
들어올 parameter의 수도 정해져야 하고 갯수가 많아질수록, 복잡해질수록 코드가 난잡해진다. 하지만 아래의 코드를 보자.
function onlyOneTrue() {
var sum = 0;
for (var i=0; i<arguments.length; i++) {
if (arguments[i]) {
sum += arguments[i];
}
}
return sum == 1;
}
물론 명시적 강제변환이라고 같은 코드를 작성하지 못하는 건 아니다.
function onlyOneTrue() {
var sum = 0;
for (var i=0; i<arguments.length; i++) {
sum += Number(!!arguments[i]);
}
return sum === 1;
}
다음은 boolean value로의 암시적 강제변환이 일어나는 상황들이다.
if()
문 내의 조건 expressionfor( ; ; )
의 두 번째 조건 expressionwhile()
혹은 do ... while()
의 조건 expression? :
삼항 연산자의 첫 번째 조건 expression||
혹은 &&
의 좌측 피연산자이 상황에서 해당 value가 boolean이 아닌 값이 들어가 있으면 모두 ToBoolean 추상 연산에 따라 암시적으로 강제변환된다.
Javascript를 많이 만져보았다면 그리 이상한 상황들은 아니라고 생각할 것이다. 그리고 ||
와 &&
의 return value가 각각의 논리 연산이 true인지 false인지를 처음으로 확인해준 값이라는 것도 다들 잘 알 것이다.
var a = 10;
var b = null;
var c = "str";
a || b; // 10
a && b; // null
b || c; // "str"
b && c; // null
이런 것을 활용해서 Guard Operator(가드 연산자)를 만들 수 있다. 아래의 코드를 보자.
function showValue() {
console.log(a);
}
var a = "some value";
a && showValue(a);
a가 있을 때에만 코드를 실행한다. 물론 if(a){ showValue(a); }
도 틀린 것은 아니지만 JS minifier에서는 어떻게든 코드를 줄이기 위해 위와 같은 코드를 사용한다.
String으로의 변환은 명시적 강제변환만 가능하다.
var symbol1 = Symbol("hi");
String(symbol1); // "Symbol(hi)"
symbol1 + ""; // TypeError
Number로는 양쪽 방향으로 모두 변환이 불가능하다.
Boolean으로의 변환은 가능하며 암시적으로나 명시적으로 다 true를 return한다.
Loose equals(==
)는 값만, Strict equals(===
)는 값과 타입을 모두 비교한다고 보통은 알려져있다. 하지만 정확히 말하면 loose equals는 동등 비교 시 암시적 강제변환을 허용하고, strict equals는 암시적 강제변환을 허용하지 않는다가 맞다. 그리고 성능상으로는 강제변환을 하는 loose equals가 더 느릴 것 같지만, 실제로는 두 비교에 걸리는 시간의 차이는 거의 없다. 그리고 !=
와 !==
는 각각의 비교를 한 뒤 그 결과만 그대로 부정한 값을 return한다.
==
가 어떻게 값을 강제변환하는지는 명세에 잘 나와있다. 하나씩 살펴보자.
String value에 대해 ToNumber()
를 해준다.
어떤 값을 Boolean과 비교하면 boolean value에 ToNumber()
메서드를 사용하여 비교한다.
null과 undefined는 서로를 loose equals를 했을 때에만 true를 반환한다. 따라서 아래와 같은 코드가 가능하다.
if (someFunction == null) {
/* ... */
}
someFunction이라는 메서드가 정의되어 있지 않거나(undefined), 의도적으로 빈 값이라면(null), 해당 코드를 실행하게 된다. 물론 명시적으로 if(a === undefined || a === null)
처럼 작성할 수도 있기는 하다.
ToPrimitive 추상연산이 동작한다. 바뀌어야 하는 목표 type이 string인 경우에는 toString()
혹은 valueOf()
가, 그 외에 number이거나 바뀌어야 하는 type이 불확실한 경우에는 valueOf()
혹은 toString()
이 사용된다. 또한, wrapper object를 강제변환하려고 하는 경우 이전에 본 unboxing이 이루어지게 된다.
여기서 조금만 더 들어가보자. 모든 object는 [[DefaultValue]]
property를 가지고 있다. 이 property는 hint를 제공하는데, hint에 담긴 value가 String
이라면 toString()
메서드를 먼저 찾고 없으면 valueOf()
를 찾는다. hint가 Number
혹은 Default
인 경우에는 valueOf()
를 찾고 없으면 toString()
을 찾는다.
그러므로 어떤 object에 대해 valueOf()
나 toString()
메서드를 overriding을 하게 되면, 해당 사항을 유의하는 것이 좋다.
var a = 1;
var b = [1];
a == b; // true
var c = Object("c"); // new String("c")와 동일
var cString = "c";
c == cString; // true
다만, 동등비교를 하는 경우에 object는 항상 valueOf()
를 return하는 것처럼 보인다. 반대편에 string이 있어도 예상과는 다르게 toString()
메서드가 아닌 valueOf()
를 호출한다. 해당 내용은 책에도 없고 명쾌하(고 명확하)게 설명해둔 블로그나 글을 찾기도 어려워 아래의 코드만 첨부한다. 명세를 뒤적여보았으나, 위에 서술한 내용들이 약간 더 상세하게 적혀있었고, hint를 기반으로 판단한다는 얘기 이외에 loose equals를 판단할 시에 항상 hint가 Number
라는 내용은 어디서도 찾기 어려웠다.
obj = {
valueOf: () => 10,
toString: () => "str"
}
"str" == obj; // false
"10" == obj; // true
10 == obj; // true
추가적으로, undefined
나 null
은 wrapper object가 따로 없으므로 Object()
로 해석된다. NaN은 애초에 NaN == NaN
이 false
임을 유의하면 좋다.
var a = null;
var aObject = Object(a); // Object()와 동일
a == aObject; // false
var b = undefined;
var bObject = Object(b); // Object()와 동일
b == bObject; // false
var c = NaN;
var cObject = Object(c); // new Number(NaN)과 동일
c == cObject; // false
"0" == false; // true
false == 0; // true
false == ""; // true
false == []; // true
"" == 0; // true
"" == []; // true
0 == []; // true
[] == ![]; // true
"" == [null]; // true
0 == "\n"; // true
0 == " \n"; // true
빠른 결론을 원하는 이들을 위해 짧게 요약하자면 아래와 같다.
true
or false
)일 가능성이 있다면 절대 ==
연산자를 사용하지 말자.[]
, 0
, ""
(여기에는 공백 문자열로 취급되는 " "
와 "\n"
의 모든 조합이 포함된다)가 될 가능성이 있다면 웬만해선 ==
연산자를 쓰지 말자.나도 지나치게 복잡한 ==
연산 logic을 깊게 파야 한다고 생각하지는 않는다. 몇 가지 유추만 하고 넘어가는 것으로 하자.
false
가 들어간 네 개의 loose equals를 보자. 우선 위에서 양쪽 피연산자 중 boolean이 있으면 ToNumber가 동작한다고 했다. false
는 Number로 바꾸면 0
이다. 빈 String ""
이든 "0"
이든 Number로 바꾸면 0
이다. []
역시 0
으로 변환된다. 그러므로 위 코드의 상단 7개의 코드는 설명이 되었다.
[] == ![]
를 보자. 우선 !
연산자 때문에 우항이 false
로 바뀐다. [] == 0
은 위에서 확인한 코드와 같다. [null]
은 바로 ToPrimitive 연산을 통해 ""
으로 바로 바뀐다. " "
나 "\n"
와 같은 공백 string의 조합은 ToNumber
를 통해 0
으로 바뀐다.
양쪽 피연산자가 강제변환을 통해 가지는 결과값이 어느 한쪽이라도 string이 아닌 경우, ToNumber로 변환을 진행하여 비교한다. 다만 양쪽 피연산자가 강제변환된 값이 string이라고 하면 string을 알파벳 순으로(Lexicogaphic) 비교한다.
var a = [21];
var b = ["22"];
a > b; // false
b > a; // true
var c = ["10"];
var d = ["9"];
c > d; // false
d > c; // true
당연히 object는 [Object object]
로 string 변환이 이루어지다보니, object끼리는 비교가 불가능하다. 그렇다고 aObj == bObj
가 true
인 것도 아니다. reference가 다르기 때문에.
추가적으로 a <= b
는 실제로 !(b < a)
로 처리된다. a >= b
는 b <= a
로 바뀐 뒤 !(a < b)
로 처리한다.
또한 대소 비교에서는 strict relational comparison은 없다. 명시적으로 동일한 type이라는 것을 확실히 하는 방법 외에 implicit coercion을 완벽히 막는 방법은 없다. 그러므로 예상치 못한 사태에 대비하기 위해선 위와 같은 내부 logic이나 예외사항들을 눈여겨보는 것도 좋지만 그 전에 ===
를 최대한 사용하고 모종의 안전장치들을 설계해두는 것이 선결되야할 것이다.
참고 서적 :
카일 심슨, 2017, 한빛미디어, 『YOU DON'T KNOW JS: 타입과 문법, 스코프와 클로저』
참고 사이트 :
JavaScript : Convert Boolean to Number / Integer
객체를 원시형으로 변환하기
stackoverflow: valueOf() vs. toString() in Javascript