Javacsript si Wierd - 1-5. Implicit Coercion(암시적 강제변환)-thumbnail

Javacsript si Wierd - 1-5. Implicit Coercion(암시적 강제변환)

Javascript 암시적 강제변환의 예시들
387

Implicit Coercion

+ ""

+ 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()을 직접 정의한 경우에는 각각의 연산 결과가 달라질 수 있으니 이 점에 유의해야할 것이다.

Boolean to Number

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;
}

to Boolean

다음은 boolean value로의 암시적 강제변환이 일어나는 상황들이다.

이 상황에서 해당 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에서는 어떻게든 코드를 줄이기 위해 위와 같은 코드를 사용한다.

Symbol to Something

String으로의 변환은 명시적 강제변환만 가능하다.

var symbol1 = Symbol("hi");

String(symbol1); // "Symbol(hi)"
symbol1 + ""; // TypeError

Number로는 양쪽 방향으로 모두 변환이 불가능하다.

Boolean으로의 변환은 가능하며 암시적으로나 명시적으로 다 true를 return한다.


Loose Equals and Stric Equals

Loose equals(==)는 값만, Strict equals(===)는 값과 타입을 모두 비교한다고 보통은 알려져있다. 하지만 정확히 말하면 loose equals는 동등 비교 시 암시적 강제변환을 허용하고, strict equals는 암시적 강제변환을 허용하지 않는다가 맞다. 그리고 성능상으로는 강제변환을 하는 loose equals가 더 느릴 것 같지만, 실제로는 두 비교에 걸리는 시간의 차이는 거의 없다. 그리고 !=!==는 각각의 비교를 한 뒤 그 결과만 그대로 부정한 값을 return한다.

==가 어떻게 값을 강제변환하는지는 명세에 잘 나와있다. 하나씩 살펴보자.

String and Number

String value에 대해 ToNumber()를 해준다.

Boolean

어떤 값을 Boolean과 비교하면 boolean value에 ToNumber() 메서드를 사용하여 비교한다.

null and undefined

null과 undefined는 서로를 loose equals를 했을 때에만 true를 반환한다. 따라서 아래와 같은 코드가 가능하다.

if (someFunction == null) {
  /* ... */
}

someFunction이라는 메서드가 정의되어 있지 않거나(undefined), 의도적으로 빈 값이라면(null), 해당 코드를 실행하게 된다. 물론 명시적으로 if(a === undefined || a === null)처럼 작성할 수도 있기는 하다.

Object

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

추가적으로, undefinednull은 wrapper object가 따로 없으므로 Object()로 해석된다. NaN은 애초에 NaN == NaNfalse임을 유의하면 좋다.

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

빠른 결론을 원하는 이들을 위해 짧게 요약하자면 아래와 같다.

나도 지나치게 복잡한 == 연산 logic을 깊게 파야 한다고 생각하지는 않는다. 몇 가지 유추만 하고 넘어가는 것으로 하자.

false가 들어간 네 개의 loose equals를 보자. 우선 위에서 양쪽 피연산자 중 boolean이 있으면 ToNumber가 동작한다고 했다. false는 Number로 바꾸면 0이다. 빈 String ""이든 "0"이든 Number로 바꾸면 0이다. [] 역시 0으로 변환된다. 그러므로 위 코드의 상단 7개의 코드는 설명이 되었다.

[] == ![]를 보자. 우선 ! 연산자 때문에 우항이 false로 바뀐다. [] == 0은 위에서 확인한 코드와 같다. [null]은 바로 ToPrimitive 연산을 통해 ""으로 바로 바뀐다. " ""\n"와 같은 공백 string의 조합은 ToNumber를 통해 0으로 바뀐다.

Comparison

양쪽 피연산자가 강제변환을 통해 가지는 결과값이 어느 한쪽이라도 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 == bObjtrue인 것도 아니다. reference가 다르기 때문에.

추가적으로 a <= b는 실제로 !(b < a)로 처리된다. a >= bb <= 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