Javacsript si Wierd - 1-4. Explicit Coercion(명시적 강제변환)-thumbnail

Javacsript si Wierd - 1-4. Explicit Coercion(명시적 강제변환)

Javascript의 명시적 강제변환의 예시들
400

Explicit Coercion

참고하고 있는 책 『YOU DON'T KNOW JS』에서는 Javascript의 Type Casting(Conversion) 혹은 Coercion(강제변환)을 두 종류로 구분하고 있다. 의도적으로 type conversion을 일으키는 것이 명백한 코드는 명시적 강제변환implicit coercion, side effect(부수효과)로 발생하는 type conversion은 암시적 강제변환explicit coercion이라고 구분한다. 이 글에서도 그 구분법을 따르도록 하자.

기본 규칙(Abstract operation추상연산)

명시적/암시적 coercion을 살펴보기 전에 내부적으로 value가 어떻게 string, number, boolean과 같은 type으로 변하는지부터 알아보자.

ToString

string이 아닌 value를 string으로 바꾸어주는 작업은 ToString이 담당한다. Primitives의 변환값은 기본적으로 지정되어 있다. 숫자의 경우 너무 큰 수에 대해서는 지수 형태로 바뀌어 string화 된다.

var a = 1*Math.pow(1000, 7);
String(a); // 1e+21

일반 object는 toString() 메서드가 [[CLASS]]를 반환한다. 물론 자신만의 toString()을 가진 object는 그 규칙에 따른다.

var objA = {}
objA.toString(); // "[object Object]"

var objB = {
  toString: () => {
    return "Hello, I'm objB."
  }
}
objB.toString(); // "Hello, I'm objB."

JSON string으로 변환하는 작업도 살펴보자. JSON-safe value로 나타내지는 값은 모두 JSON.stringify()로 stringify가 가능하다. JSON-safe value가 아닌 녀석들은 undefined, function, symbol, cicular reference object(환형 참조 객체; property 참조가 무한히 계속되는 구조를 가진 object를 일컫는다)다.

따라서 JSON-unsafe value가 parameter에 들어오면 JSON.stringify()가 예상하기 어렵게 동작한다. undefined, function, symbol은 누락시켜 버린다. Parameter로 들어온 배열에 해당 값들이 들어있으면 null로 변환한다. Paramter로 들어온 객체에 해당 값들이 있으면 지워버린다. Circular reference object가 있으면 error를 발생시킨다.

JSON.stringify(undefined); // undefined
JSON.stringify(function(){}); // undefined

JSON.stringify([1, undefined, Symbol("test"), 4]); // "[1, null, null, 4]"

JSON.stringify({a: 1, b: 2, c: function(){}, d: Symbol("test")}); // "{a: 1, b: 2}"

JSON.stringify()에 대해 조금만 더 자세히 들어가자. JSON.stringify()의 두 번째 parameter를 전달해 filtering을 할 수도 있다. 또한 세 번째 parameter에는 들여쓰기를 위한 spacing 방식을 지정해줄 수 있다.

var obj = {
  a: 1,
  b: "b",
  c: [3, 3, 3]
};

JSON.stringify(obj, ["a", "b"]); // "{a: 1, b: "b"}"
JSON.stringify(obj, function(v, i){
  if(v !== "c") return i;
}); // "{a: 1, b: "b"}"

JSON.stringify(obj, null, "----");
// {
// ----"a": 1,
// ----"b": "b",
// ----"c": [
// --------3,
// --------3,
// --------3
// ----]
// }

비슷한 역할을 하는 메서드로 toJSON()이 있다. 이 메서드는 해당 object의 JSON-safe value를 return하는 역할을 한다. Circular reference object를 예시로 들어보자.

var badObj = {};
var obj = {
  a: 1,
  b: badObj,
  c: function() {}
};

badObj.a = obj;

JSON.stringify(obj); // TypeError

obj.toJSON = function() {
  return {a: this.a};
};

JSON.stringify(obj); // {a: 1}

toJSON()은 JSON-safe value로 변환하는 역할을, JSON.stringify()는 JSON-safe value를 string으로 변환하는 역할을 한다는 것을 기억하자.

ToNumber

예시만 간단하게 보고 넘어가자.

Number(true); // 1
Number(false); // 0
Number(null); // 0
Number(undefined); // NaN

Number(""); // 0
Number([]); // 0
Number(["a"]) // NaN
Number({}); // NaN
Number({a: 1}) // NaN

ToBoolean

falsy value

여기에 없는 값이면 전부 truthy value다. 즉, object는 전부 truthy다.

그런데 실제로는 falsy object도 존재한다. 대표적인 예시는 document.all이라고 한다. document.all은 array-like object로, 웹 페이지 요소들을 가져올 수 있게 해주었다. 지금의 document.querySelectordocument.getElementById와 같은 역할을 했던 것이다.

해당 값은 주로 IE를 감지하는 데에 쓰였다. IE가 웹 표준을 준수하지 않는 대표적인 브라우저였기 때문이다. IE에서는 document.all을 사용해 웹 페이지 요소를 가져옴으로써 웹 페이지에 다양한 동적 효과를 줄 수 있었다.

if (document.all) {
  /* do something for old IE with document.all */
} else {
  /* do something for other browsers with document.querySelector & document.getElementById, etc */
}

그런데 한참의 시간이 지난 후 IE는 웹 표준을 준수하기 시작했고, 더 이상 저 코드가 돌아가게 두면 안 되었지만 동시에 document.all을 코드에서 완전히 없애버리는 것도 어려운 일이었다. 그 상황에서 생각해낸 해결책은 document.all을 falsy로 만드는 것이었다. if문 안에 document.all을 사용한 코드를 다 집어넣은 뒤 else 부분에서 document.querySelectordocument.getElementById 등을 사용해 웹 표준을 준수하는 브라우저들에서 같은 효과를 주는 코드를 넣어두면 되었다.

그렇게해서 document.all은 falsy object로 남(아야만 하)게 되었다.

명시적 강제변환(Explicit Coercion)

basic(String, Number)

var a = 10;
String(a); // "10"
a.toString(); // "10"

var b = "3.14";
Number(b); // 3.14
+b; // 3.14

var timestamp = +new Date();

String()Number()가 명시적이라는 것에는 이견이 없을 것이다.

toString()은 약간의 암시적인 측면이 숨겨져 있다. 이전에 이야기했던 것과 마찬가지로 string에는 없는 메서드이므로 javascript engine은 wrapper object로 "10"을 boxing한 뒤에 toString() 메서드를 사용할 수 있도록 한다.

+는 unary operator(단항 연산자; 피연산자가 하나뿐인 연산자)다. ToNumber abstract operation(추상연산)에 따라 number value로 변환시킨다. 예시에서는 string과 date value를 number value로 변환시켰다.

참고로 new로 constructor를 호출할 시, 전달되는 인자가 없으면 ()를 생략하는 것이 가능하다.

var timestamp = +new Date;

Number로 바꾸는 형태들에 대해서 좀 더 알아보자. 이번에는 parsing과 강제 변환의 차이를 체크해보자.

var a = "100px";

Number(a); // NaN
parseInt(a); // 100

강제변환을 하는 경우 non-numeric character(비숫자형 문자)를 허용하지 않아 NaN을 return한다. 하지만 parsing은 parameter로 들어온 문자열을 왼쪽에서 오른쪽으로 훑으며 숫자로 parsing할 수 없는 값을 만나면 멈추고 그때까지의 값을 return한다.

참고로 parseInt() 메서드는 string에 사용하는 것임을 명심하자. Parameter로 들어온 value가 string이 아니면 먼저 string으로 강제변환이 이루어진다. 일종의 암시적 강제변환implicit coercion이라 할 수 있는데, 이런 상황이 달갑지 않다는 것은 누구나 동의할 수 있을 것이다.

ES5 이전에는 parseInt() 메서드의 두 번째 parameter가 비어있을 경우, 첫 번째 parameter인 string의 첫 번째 문자만 보고 멋대로 유추하는 문제가 있었다. 첫 번째 문자가 x면 16진수, 0이면 8진수로 해석하는 것이다. 따라서 그 이전에는 안전하게 parseInt(str, 10)의 형태로 적어주어야 했다. 지금은 둘 다 바뀌었지만, 여전히 0x로 시작하는 문자는 16진수로 변환시켜버리므로 주의해야 할 것이다.

parseInt("x11"); // NaN
parseInt("0x11"); // 17
parseInt("0x11", 16); // 17
parseInt("011"); // 11
parseInt("011", 8); // 9

두 번째 인자 얘기를 조금만 더 이어나가보자. 아래의 코드를 보자.

parseInt(1/0, 19); // 18

이게 대체 어떻게 된 일일까? 우선, 1/0은 Infinity가 될 것이다(여기서부터 문제가 있다고 느꼈다면, 맞다. String이 아닌 값을 첫 번째 parameter로 전달했다). 위에서 설명한 것처럼 Infinity가 암시적으로 stringify되면 "Infinity"다. 19진법에서 I는 10진법의 18과 같다. n는 19진법에서는 없는 숫자이므로 여기서 parsing을 끝내고 18을 return한다. 이 원리만 기억하면 아래에 제시된 parseInt()의 행동이 전부 이해가 갈 것이다.

parseInt(0.000009); // 0
parseInt(0.0000009); // 9
parseInt(false, 16); // 250
parseInt(parseInt, 16); // 15
parseInt("0x10"); // 16
parseInt("109", 2); // 2

parseInt(parseInt, 16);에 힌트를 약간 주자면, parseInt는 function이므로 아마 그 값은 function...의 형태일 것이다.

비트 연산자

이번에는 좀 어렵고 생소한 개념들을 보자. 바로 OR 연산자(|)와 틸드(tilde, ~)에 관한 이야기이다.

우선 OR 연산자를 보자. 원래 OR 연산자는 주어진 두 수의 32bit 연산을 수행하며 둘 중 하나라도 해당 자릿수가 1이면 1을 return한다. 예시를 보자면 아래와 같다.

var a = 5; // 101
var b = 3; // 011

a | b; // 111, 즉 10진수 7

이걸 응용하면 ToInt32 abstract operation을 손쉽게 명시적으로 행할 수 있다.

0 | -0; // 0
0 | NaN; // 0
0 | Infinity; // 0

0은 32bit에서도 모든 자리수가 0이기 때문에 사실상 아무런 연산도 이루어지지 않은 것과 같다. 하여, 0 | x에서 x 자리에 있는 value에 ToInt32 변환만 행한 것과 같은 결과를 얻게 된다.

둘째로 tilde에 대해서 보자. Tilde는 해당 value를 32bit 숫자로 강제변환한 뒤 NOT 연산을 한다. NOT 연산은 각 자리수의 value를 반대로 바꾼다.

var a = 7; // 0111
~a; // 1000, 10진수의 -8

수학적으로 말하자면 2의 보수complement를 구하는데, ~x-(x+1)과 같다. 이를 이용해보자.

프로그래밍에서는 종종 경계값Sentinel value을 확인할 수 있다. 그 대표적인 값은 -1이다. 보통 어떤 함수가 -1을 return한다는 것은 해당 함수가 원하는 요구사항을 만족시키지 못했다는 것을 뜻한다. indexOf() 메서드를 보자.

var str = "Hi, how are you?";
if (str.indexOf("bad word") != -1) {
  /* filtering... */
}

입력된 string에 filtering해야할 단어가 있으면 실행되는 코드다. 그런데, ~(-1)은 0이고, falsy value다. 그러므로 해당 코드는 좀 더 축약되어 이렇게 쓰일 수 있다.

if (~str.indexOf("bad word")) {
  /* filtering... */
}
if (!~str.indexOf("bad word")) {
  /* use string as it is... */
}

Tilde를 두 번 쓰면 또 다른 용도로 tilde를 활용할 수 있다. 맨 앞의 ~가 ToInt32 강제변환 후 bit를 거꾸로 바꾼다. 그 다음의 ~가 bit를 다시 거꾸로 돌린다. 결국 이루어지는 것은 ToInt32 강제변환일 뿐이다. 이를 교묘하게 이용하면 소수점 이상 부분만 잘라내버릴 수 있다.

Math.floor(5.6); // 5
~~5.6; // 5

Math.floor(-5.6); // -6
~~-5.6; // -5

물론 '내림'과는 개념이 다르다는 것을 위의 코드에서 확인할 수 있다.

그렇다면 아까 본 0 | x(혹은 x | 0)와는 뭐가 다른가? 미세한 차이지만 연산 우선순위 때문에 ~~가 유용할 수 있다.

1E20; // 100000000000000000000
~~1E20; // 1661992960
1E20 | 0; // 1661992960

~~1E20 / 10; // 166199296
1E20 | 0 / 10; // 1661992960
(1E20 | 0) / 10; // 166199296

Boolean

Falsy value를 제외한 나머지 value들은 명시적 강제변환explicit coercion을 통해 true로 바뀐다. Falsy value들은 당연히 false로 바뀐다.

Boolean(""); // false
Boolean([]); // true

!!{}; // true
!!0; // false

Boolean()!!가 주로 사용된다.


참고 서적 :
카일 심슨, 2017, 한빛미디어, 『YOU DON'T KNOW JS: 타입과 문법, 스코프와 클로저』