First written: 23-01-20
Uploaded: 23-02-27
Last modified: 23-02-28
참고하고 있는 책 『YOU DON'T KNOW JS』에서는 Javascript의 Type Casting(Conversion) 혹은 Coercion(강제변환)을 두 종류로 구분하고 있다. 의도적으로 type conversion을 일으키는 것이 명백한 코드는 명시적 강제변환implicit coercion, side effect(부수효과)로 발생하는 type conversion은 암시적 강제변환explicit coercion이라고 구분한다. 이 글에서도 그 구분법을 따르도록 하자.
명시적/암시적 coercion을 살펴보기 전에 내부적으로 value가 어떻게 string, number, boolean과 같은 type으로 변하는지부터 알아보자.
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으로 변환하는 역할을 한다는 것을 기억하자.
예시만 간단하게 보고 넘어가자.
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
여기에 없는 값이면 전부 truthy value다. 즉, object는 전부 truthy다.
그런데 실제로는 falsy object도 존재한다. 대표적인 예시는 document.all
이라고 한다. document.all
은 array-like object로, 웹 페이지 요소들을 가져올 수 있게 해주었다. 지금의 document.querySelector
나 document.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.querySelector
나 document.getElementById
등을 사용해 웹 표준을 준수하는 브라우저들에서 같은 효과를 주는 코드를 넣어두면 되었다.
그렇게해서 document.all
은 falsy object로 남(아야만 하)게 되었다.
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
Falsy value를 제외한 나머지 value들은 명시적 강제변환explicit coercion을 통해 true로 바뀐다. Falsy value들은 당연히 false로 바뀐다.
Boolean(""); // false
Boolean([]); // true
!!{}; // true
!!0; // false
Boolean()
과 !!
가 주로 사용된다.
참고 서적 :
카일 심슨, 2017, 한빛미디어, 『YOU DON'T KNOW JS: 타입과 문법, 스코프와 클로저』