🌈 Chapter 4 : 강제변환
Table of Contents
🎯 값 변환
- 어떤 값을 다른 타입의 값으로 바꾸는 과정이 명시적이면 타입 캐스팅(Type Casting), 암시적이면 강제변환(Coercion) 이라고 한다.
- 항상 그렇지 않을 수도 있지만 자바스크립트에서 강제변환을 하면 문자열, 숫자, 불리언 같은 스칼라 원시 값 중 하나가 되며, 객체, 함수 같은 합성 값 타입으로 변환될 일은 없다.
- 이렇게도 구분할 수 있는데, 타입 캐스팅(타입 변환)은 정적 타입 언어에서 컴파일 시점에, 강제변환은 동적 타입 언어에서 런타임 시점에 발생한다.
- 그러나 자바스크립트에서는 대부분 모든 유형의 타입변환을 강제변환으로 뭉뚱그려 일컽는 경향이 있어서, 암시적 강제변환과 명시적 강제변환 두 가지로 구별할 수도 있다.
- 명시적 강제변환은 의도적으로 타입변환을 일으킨다는 사실이 명백한 반면, 암시적 강제변환은 다른 작업 도중 불분명한 부수 효과(Side Effect)로부터 발생하는 타입변환이다.
var a = 42;
var b = a + ''; // 암시적 강제변환
var c = String(a); // 명시적 강제변환
- 둘의 차이는 엄밀히 말해 스타일의 차이뿐 아니라 작동상에도 미묘한 차이가 있다.
- 명시적(Explicit) : 암시적(Implicit) = 명백한(Obvious) : 숨겨진 부수 효과(Hidden Side Effect) 용어상으로는 이러한 대응 관계가 성립
🎯 추상 연산
📚 ToString
- 문자열이 아닌 값 -> 문자열 변환 작업은 ES5 9.8의
ToString
추상 연산 로직이 담당한다.
var a = 1.07 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000;
a; // 1.07e+21
a.toString(); // "1.07e+21"
- 일반 객체는 특별히 지정하지 않으면 기본적으로
toString()
메서드가 내부[[Class]]
를 반환한다. - 자신의
toString()
메서드를 가진 객체는 문자열처럼 사용하면 자동으로 이 메서드가 기본 호출되어toString()
을 대체한다. - 배열은 기본적으로 재정의된
toString()
이 있다. 문자열 변환 시 모든 원소 값이 콤마(.)로 분리된 형태로 이어진다.
var a = [1, 2, 3];
a.toString(); // "1,2,3"
- 또한
toString()
메서드는 명시적으로도 호출 가능하며, 문자열 콘텍스트에서 문자열 아닌 값이 있을 경우에도 자동 호출된다.
📌 JSON 문자열화
ToString
은JSON.stringify()
유틸리티를 사용하여 어떤 값을JSON
문자열로 직렬화하는 문제와도 연관된다.JSON
문자열화는 강제변환과 똑같지는 않지만,ToString
규칙과 관련이 있다.- 대부분 단순 값들은 직렬화 결과가 반드시 문자열이라는 점을 제외하고는,
JSON
문자열화나toString()
변환이나 기본적으로 같은 로직이다.
JSON.stringify(42); // "42"
JSON.stringify("42"); // ""42""
JSON.stringify(null); // "null"
JSON.stringify(true); // "true"
JSON
안전 값(JSON
표현형으로 확실히 나타낼 수 있는 값)은 모두JSON.stringify()
로 문자열화할 수 있다.JSON
안전 값이 아닌 것들은undefined
, 함수, 심벌, 환형 참조 객체(프로퍼티 참조가 무한 순환되는 구조의 객체) 같은 것들이다.- 만약
JSON.stringify()
는 안전 값이 아닌 것들은 자동으로 누락시키며 만약 배열에 포함되어 있으면null
로 바꾼다. 객체 프로퍼티에 있으면 지워버린다.
JSON.stringify(undefined); // undefined
JSON.stringify(function(){}); // undefined
JSON.stringify([1, undefined, function(){}, 4]); // "[1,null,null,4]"
JSON.stringify({ a:2, b: function(){} }) // "{"a":2}"
- 환형 참조 객체를 넘기면 에러가 발생한다.
- 객체 자체에
toJSON()
메서드가 정의되어 있다면 먼저 이 메서드를 호출하여 직렬화한 값을 반환한다. - 부적절한
JSON
값이나 직렬화하기 곤란한 객체 값을 문자열화하려면toJSON()
메서드를 따로 정의해야 한다.
var o = {};
var a = {
b: 42,
c: o,
d: function(){}
};
// a를 환형 참조 객체로 만든다.
o.e = a;
// 에러 발생
JSON.stringify(a); // Uncaught TypeError: Converting circular structure to JSON
// JSON 값으로 직렬화하는 함수를 따로 정의한다.
a.toJSON = function() {
return { b: this.b }; // 직렬화에 b만 포함시킨다.
}
JSON.stringify(a); // "{"b":42}"
toJSON()
은 적절히 평범한 실제 값을 반환하고 문자열화 처리는JSON.stringify()
이 담당한다.- 다시 말해
toJSON()
의 역할은 문자열화하기 적당한JSON
안전 값을 바꾸는 것이지,JSON
문자열로 바꾸는 것이 아니다.
var a = {
val: [1, 2, 3],
// 맞다
toJSON: function() {
return this.val.slice(1);
}
};
var b = {
val: [1, 2, 3],
// 틀리다
toJSON: function() {
return '[' + this.val.slice(1).join() + ']';
}
};
JSON.stringify(a); // "[2,3]"
JSON.stringify(b); // ""[2,3]""
- 배열 아니면 함수 형태의 대체자를
JSON.stringify()
의 두 번째 선택 인자로 지정하여 객체를 재귀적으로 직렬화하면서 필터링 하는 방법이 있다.toJSON()
이 직렬화할 값을 준비하는 방식과 비슷하다. - 대체자가 배열이면 전체 원소는 문자열이어야 하고 각 원소는 객체 직렬화의 대상 프로퍼티명이다. 즉, 여기에 포함되지 않은 프로퍼티는 직렬화 과정에서 빠진다.
- 대체자가 함수면 처음 한 번은 객체 자신에 대해, 그 다음엔 각 객체 프로퍼티별로 한 번씩 실행하면서 매번 키와 값 두 인자를 전달한다. 직렬화 과정에서 해당 키를 건너뛰려면
undefined
를 그 외엔 해당 값을 반환한다.
var a = {
b: 42,
c: '42',
d: [1, 2, 3]
};
JSON.stringify(a, ['b', 'c']); // "{"b":42,"c":"42"}"
JSON.stringify(a, function(k, v) {
if(k !== 'c') return v;
}); // "{"b":42,"d":[1,2,3]}"
- 함수인 대체자 는 최초 호출 시 키 인자
k
는undefined
이다. - 대체자는
if
문에서 키가c
인 프로퍼티를 솎아내고 문자열화는 재귀적으로 이루어지므로 배열[1,2,3]
의 각 원소는v
(1,2,3)로, 인덱스는k
(0,1,2)로 각각 대체자 함수에 전달된다. JSON.stringify()
의 세 번째 선택 인자는 스페이스라고 하며 사람이 읽기 쉽도록 들여쓰기를 할 수 있다.- 들여 쓰기를 할 빈 공간의 개수를 숫자로 지정하거나 문자열(10자 이상이면 앞에서 10자까지만 잘라 사용한다)을 지정하여 각 들여 쓰기 수준에 사용한다.
var a = {
b: 42,
c: '42',
d: [1, 2, 3]
};
JSON.stringify(a, null, 3);
// "{
// "b": 42,
// "c": "42",
// "d": [
// 1,
// null,
// 3
// ]
// }"
JSON.stringify(a, null, '-----');
// "{
// -----"b": 42,
// -----"c": "42",
// -----"d": [
// ----------1,
// ----------null,
// ----------3
// -----]
// }"
JSON.stringify()
는 직접적인 강제변환의 형식은 아니지만 두 가지 이유로ToString
강제변환과 연관된다.- 문자열, 숫자, 불리언,
null
값이JSON
으로 문자열화하는 방식은ToString
추상 연산의 규칙에 따라 문자열 값으로 강제변환되는 방식과 동일하다. JSON.stringify()
에 전달한 객체가 자체toJSON()
메서드를 갖고 있다면, 문자열화 전toJSON()
이 자동 호출되어JSON
안전 값으로 강제변환된다.
- 문자열, 숫자, 불리언,
📚 ToNumber
- 숫자가 아닌 값 -> 수식 연산이 가능한 숫자 변환 로직은 예를 들어
true
는 1,false
는 0이 되고,undefined
는NaN
으로,null
은 0으로 바뀐다. - 문자열 값에
ToNumber
를 적용하면 대부분 숫자 리터럴 규칙/구문과 비슷하게 작동한다. - 변환이 실패하면 결과는
NaN
이다. - 한 가지 차이는 0이 앞에 붙은 8진수는
ToNumber
에서 올바른 숫자 리터럴이라도 8진수로 처리하지 않는다.(10진수로 처리) - 객체(또는 배열)는 동등한 원시 값으로 변환 후 그 결괏값(아직 숫자가 아닌 원시 값)을 앞서 설명한
ToNumber
규칙에 의해 강제변환한다. - 동둥한 원시 값으로 바꾸기 위해
ToPrimitive
추상 연산 과정에서 해당 객체가valueOf()
메서드를 구현했는지 확인한다. valueOf()
를 쓸 수 있고 반환 값이 원시 값이면 그대로 강제변환하되, 그렇지 않을 경우 (toString()
메서드가 존재하면)toString()
을 이용하여 강제변환한다.- 원시 값으로 바꿀 수 없을 땐
TypeError
오류를 던진다. - ES5부터는
[[Prototype]]
가null
인 경우 대부분Object.create(null)
를 이용하여 강제변환이 불가능한 객체(valueOf()
,toString()
메서드가 없는 객체)를 생성할 수 있다.
var a = {
valueOf: function() {
return '42';
}
};
var b = {
toString: function() {
return '42';
}
};
var c = [4, 2];
c.toString = function() {
return this.join(''); // '42'
};
Number(a); // 42
Number(b); // 42
Number(c); // 42
Number(''); // 0
Number([]); // 0
Number(['abc']); // NaN
📚 ToBoolean
- 1을
true
로, 0을false
로 강제변환할 수는 있지만 그렇다고 두 값이 똑같은 건 아니다.
📌 Falsy 값
true/false
가 아닌 값을 불리언에 상당한 값으로 강제변환했을 때, 이 값들은?- 자바스크립트 모든 값은 다음 둘 중 하나다.
- 불리언으로 강제변환하면
false
가 되는 값 - 1번을 제외한 나머지(즉, 명백히
true
인 값)
- 불리언으로 강제변환하면
- 명세가 정의한
falsy
값은undefined
,null
,false
,+0
,-0
,NaN
,''
이다. - 위
falsy
값은 불리언으로 강제변환하면false
이다. - 반대로 위 값들 중 없으면
truthy
이다. 하지만 자바스크립트 명세에는truthy
값 목록 같은 건 없다. - 모든 객체는 명백히
truthy
하다는 식의 몇 가지 예시만 있을 뿐falsy
값 목록에 없으면 응당truthy
값이 되는 것이다.
📌 Falsy 객체
var a = new Boolean(false);
var b = new Number(0);
var c = new String('');
var d= Boolean(a && b && c);
d; // true
- a, b, c는 명백히
falsy
값을 감싼 객체이다. - d가
true
인 것으로 봐서 세 변수는 모두true
이다. - 일반적인 자바스크립트의 의미뿐 아니라 브라우저만의 특이한 작동 방식을 가진 값을 생성하는 경우가 있는데, 이것이 바로
falsy
객체의 정체이다. falsy
객체는 불리언으로 강제변환하면false
이다.
📌 truthy 값
var a = 'false';
var b = '0';
var c = "''";
var d = Boolean(a && b && c);
d; // true
- 문자열 값을 보면
falsy
처럼 보이지만 문자열 값 자체는 모두truthy
이기 때문이다.
var a = []; // 빈 배열
var b = {}; // 빈 객체
var c = function(){}; // 빈 함수
var d = Boolean(a && b && c);
d; // true
- 외향은
falsy
처럼 생겼지만 어떻든[]
,{}
,function(){}
는falsy
값 목록에 없으므로 모두truthy
값이다. truthy
값 목록은 사실상 무한하여 일일이 작성하는 게 불가능하다.truthy/falsy
개념은 어떤 값을 불리언 타입으로 (명시적/암시적) 강제변환 시 해당 값의 작동 방식을 이해한다는 점에서 중요하다.
🎯 명시적 강제변환
- 명시적 강제변환(Explicit Coercion)은 분명하고 확실한 타입변환이다.
📚 명시적 강제변환: 문자열 ↔ 숫자
- 문자열 ↔ 숫자 강제변환은
String()
과Number()
함수를 이용하는데, 앞에new
키워드가 붙지 않기 때문에 객체 래퍼를 생성하는 것이 아니란 점이다.
var a = 42;
var b = String(a);
var c = '3.14';
var d = Number(c);
b; // '42'
d; // 3.14
ToString
추상 연산 로직에 따라String()
은 값을 받아 원시 문자열로 강제 변환한다.Number()
역시 마찬가지로ToNumber
추상 연산 로직에 의해 어떤 값이든 원시 숫자 값으로 강제변환한다.String()
과Number()
이외에도 문자열 ↔ 숫자의 명시적인 타입변환 방법은 또 있다.
var a = 42;
var b = a.toString();
var c = '3.14';
var d = +c;
b; // '42'
d; // 3.14
a.toString()
호출은 겉보기엔 명시적이지만, 몇 가지 암시적인 요소가 감춰져 있다.- 원시 값 42에는
toString()
메서드가 없으므로 엔진은toString()
를 사용할 수 있게 자동으로 42를 객체 래퍼로 박싱한다. (말하자면 명시적으로, 암시적인 작동이다.) +c
의+
는 단항연산자로 피연산자c
를 숫자로, 명시적 강제변환한다.
var c = '3.14';
var d = 5+ +c;
d; // 8.14
-
단항 연산자 역시+
처럼 강제변환을 하지만 숫자의 부호를 뒤바꿀 수도 있다.
1 + - + + + - + 1; // 2
- 명시적으로 변환하여 문제를 악화시키지 말고 혼동을 줄이는 게 좋다.
📌 날짜 ➡ 숫자
+
단항 연산자는Date
객체 ➡ 숫자 강제변환 용도로 쓰인다.- 결괏값이 날짜/시각 값을 유닉스 타임스탬프 표현형이기 때문이다.
var d = new Date();
+d; // 1604492699962
// 현재 시각을 타임스탬프로 바꿀 때 관용적으로 사용하는 방법
var timestamp = +new Date();
- 그러나 강제변환을 하지 않고도
Date
객체로부터 타임스탬프를 얻는 방법이 있다. - 오히려 강제변환을 하지 않은 쪽이 더 명시적이므로 권장할 만하다.
var timestamp = new Date().getTime();
// ES5에 추가된 정적 함수 Date.now()를 쓰는 게 더 낫다.
var timestamp = Date.now();
- 날짜 타입에 관한 한 강제변환은 권하지 않고 현재 타입스탬프는
Date.now()
로, 그 외 특정 날짜/시간의 타임스탬프는new Date().getTime()
를 대신 사용하자.
📌 이상한 나라의 틸드(~)
- 앞에서 자바스크립트 비트 연산자는 오직 32비트 연산만 가능하다고 했다. 즉, 비트 연산을 하면 피연산자는 32 비트 값으로 강제로 맞춰지는데, ToInt32 추상 연산이 이 역할을 맡는다.
- 우선 ToInt32은
ToNumber
강제변환하고,"123"
이라면 ToInt32 규칙을 적용하기 전123
으로 바꾼다. - 엄밀히 말해 이 자체는 강제변환이 아니지만, 숫자 값에
|
나~
비트 연산자를 적용하면 전혀 다른 숫자 값을 생성하는 강제변환 효과가 있다. - 예를 들어 아무 연산도 하지 않는 0 | x의 OR연산자(|)는 사실상 ToInt32 변환만 수행한다.
0 | -0; // 0
0 | NaN; // 0
0 | Infinity; // 0
0 | -Infinity; // 0
- 이러한 특수 숫자들은 32비트로 나타내는 것이 불가능하므로 ToInt32 연산 결과는 0 이다.
~
연산자는 먼저 32 비트 숫자로 강제변환한 후NOT
연산을 한다.- 이산 수학에서
~
는 2의 보수를 구한다. - 즉,
~x
는-(x+1)
와 같다.
~42; // -(42+1) ==> -43
- 위 연산에서 결과를 0으로 만드는 유일한 값은 -1이다.
- 다시 말해, 일정 범위 내의 숫자 값에
~
연산을 할 경우 입력 값이 -1이면 (false
로 쉽게 강제변환할 수 있는)falsy
한 0, 그 외엔truthy
한 숫자 값이 산출된다. - 여기서 -1과 같은 성질의 값을 경계 값(Sentinel Value) 라고 하는데, 동일 타입(숫자)의 더 확장된 값의 집합 내에서 임의의 어떤 의미를 부여한 값이다.
- 자바스크립트는 문자열 메서드
indexOf()
를 정의할 때 이 전례에 따라 특정 문자를 검색하고 발견하면 0부터 시작하는 숫자 값을, 발견하지 못했을 경우 -1을 반환한다.
var a = 'Hello World';
if(a.indexOf('lo') >= 0) { // true
// found it
}
if(a.indexOf('lo') != -1) { // true
// found it
}
if(a.indexOf('ol') < 0) { // true
// not found
}
if(a.indexOf('ol') == -1) { // true
// not found
}
- 많이 지저분해 보인다. 기본적으로 이런 부류의 코드는 구멍 난 추상화(Leaky Abstraction), 즉 내부 구현 방식을 내가 짠 코드에 심어놓은 꼴이다.
var a = 'Hello World';
~a.indexOf('lo'); // -4 <-- truthy!
if(~a.indexOf('lo')) { // true
// found it
}
~a.indexOf('ol'); // 0 <-- falsy!
!~a.indexOf('lo'); // true
if(~a.indexOf('ol')) {// true
// not found
}
📌 비트 잘라내기
~
의 용도가 하나 더 존재한다.- 숫자의 소수점 이상 부분을 잘라내기 위해 더블 틸드(
~~
)를 사용하는 개발자들이 있는데 흔히, 이렇게 하면Math.floor()
와 같은 결과가 나온다고 생각한다. ~~
는 처음~
는 ToInt32 강제변환을 적용한 후 각 비트를 거꾸로한다.- 그리고 두 번째
~
는 비트를 또 한 번 뒤집는데, 결과적으로 원래 상태로 되돌린다. - 결국 ToInt32 강제변환(잘라내기)만 하는 셈이다.
- 우선
~~
연산은 32 비트 값에 한아여 안전하지만 그보다도 음수에서는Math.floor()
과 결괏값이 다르다는 사실에 조심해야 한다.
Math.floor(-49.6); // -50
~~-49.6 // -49
Math.floor()
과 다른 점은 차치하더라도~~x
는(32비트) 정수로 상위 비트를 잘라낸다.- 하지만 같은 일을 하는
x | 0
가(조금이라도) 더 빠를 것 같다. 그러면 왜~~x
를 써야 할까? - 바로 연산자 우선순위 때문이다.
~~1E20 / 10; // 166199296
1E20 | 0 / 10; //1661992960
(1E20 | 0) / 10; // 166199296
📚 명시적 강제변환: 숫자 형태의 문자열 파싱
var a = '42';
var b = '42px';
Number(a); // 42
parseInt(a); // 42
Number(b); // NaN
parseInt(b); // 42
- 문자열로 부터 숫자 값의 파싱은 비 숫자형 문자를 허용한다.
- 즉, 좌 ➡ 우 방향으로 파싱하다가 숫자 같지 않은 문자를 만나면 즉시 멈춘다.
- 반면 강제변환은 비 숫자형 문자를 허용하지 않기 때문에
NaN
를 리턴한다. - 그렇기 때문에 파싱은 강제변환의 대안이 될 수 없다.
- 우측에 비 숫자형 문자가 있을지 확실하지 않거나 별로 상관 없다면 문자열을 숫자로 파싱한다.
parseInt()
는 문자열에 쓰는 함수이다. 인자가 숫자라면 애당초parseInt()
를 쓸 이유가 없다.- 인자가 비 문자열이면 제일 먼저 자동으로 문자열로 강제변환한다.
- 절대로
parseInt()
에 비 문자열 값을 넘기지 말아야 한다.
📌 비 문자열 파싱
parseInt(1/0, 19); // 18
- 위 코드는 무한대를 정수로 파싱하면 당연히 무한대로 나와야 하지만 결과는 18이 나왔다.
- 먼저, 비 문자열을
parseInt()
첫 번째 인자로 넘긴 것 자체가 잘못되었다. - 하지만 이런 상황이 닥쳐와도 자바스크립트 엔진은 비 문자열을 문자열로 최대한 강제변환하려고 노력한다.
parseInt(new String('42')); // 42
- 위 코드는 비 문자열 인자를 받았으니 실행되지 말아야 할까?
String
객체 래퍼가42
로 언박싱되기를 바란다면, 42가 먼저"42"
가 된 다음, 다시 42로 파싱되어 반환되는 게 정말 이상한 일인가?
var a = {
num: 21,
toString: function() { return String(this.num * 2); }
};
parseInt(a); // 42
- 인자 값을 강제로 문자열로 바꾼 다음 파싱을 시작하는
parseInt()
의 로직은 상당히 일리가 있다. - 만약 무한대(1/0) 같은 값을 넘긴다면 어떤 문자열로 변환하는 것이 최선일까?
- 자바스크립트는
Infinity
를 택했다. - 그러면
parseInt(1/0, 19)
는parseInt('Infinity', 19)
인데, 어떻게 파싱되는 것일까? - 첫 번째 문자
I
는 19진수 18에 해당한다. 두 번째n
은0-9
,a-i
범위 밖의 문자이므로 파싱은 여기서 멈춘다. - 그래서 결과는 18인 것이다.
parseInt(0.000008); // 0
parseInt(0.0000008); // 8 ('8e-7' -> '8')
parseInt(false, 16); // 250 ('false' -> 'fa')
parseInt(parseInt, 16); // 15 ('function...', -> 'f')
parseInt('0x10'); // 16
parseInt('103', 2); // 2
📚 명시적 강제변환: *
➡ 불리언
String()
,Number()
도 그렇듯이Boolean()
명시적인 강제변환 방법이다.(앞에new
x)
var a = '0';
var b = [];
var c = {};
var d = '';
var e = 0;
var f = null;
var g;
Boolean(a); // true
Boolean(b); // true
Boolean(c); // true
Boolean(d); // false
Boolean(e); // false
Boolean(f); // false
Boolean(g); // false
Boolean()
은 명시적이지만 자주 쓰지이지 않는다.+
단항 연산자가 값을 숫자로 강제변환하는 것처럼!
부정 단항 연산자도 값을 불리언으로 명시적으로 강제변환한다.- 문제는 그 과정에서
truthy
,falsy
까지 뒤바뀐다. - 그래서 일반적으로 자바스크립트 개발 시 불리언 값으로 명시적인 강제변환을 할 땐
!!
이중 부정 연산자를 사용한다. - 두 번째
!
이 패리티를 다시 원상 복구 한다.
var a = '0';
var b = [];
var c = {};
var d = '';
var e = 0;
var f = null;
var g;
!!a; // true
!!b; // true
!!c; // true
!!d; // false
!!e; // false
!!f; // false
!!g; // false
- 이 같은
ToBoolean
강제변환 모두Boolean()
이나!!
를 쓰지 않으면if()
문 등의 불리언 콘텍스트에서 암시적인 강제변환이 일어난다. - 여기서는
ToBoolean
강제변환의 원래 의도를 좀 더 명확히 구현하기 위해 값을 불리언으로 명시적인 강제변환을 했다. - 자료 구조의
JSON
직렬화 시true/false
값으로 강제변환하는 것도 명시적인ToBoolean
강제변환의 일례이다.
var a = [
1,
function(){/* ... */},
2,
function(){/* ... */},
];
JSON.stringify(a); // "[1,null,2,null]"
JSON.stringify(a, function(key, val) {
if(typeof val == 'function') {
// 함수를 ToBoolean 강제변환한다.
return !!val;
}
else {
return val
}
});
// "[1,true,2,true]"
- 다음 코드는
true/false
중 하나가 결과가 도출된다는 점에서 명시적인ToBoolean
강제변환의 모습과 닮았다.
var a = 42;
var b = a ? true : false;
- 그러나 여기엔 암시적 강제변환이 매복해 있다.
- a를 일단 불리언으로 강제변환해야 표현식 전체의
true/false
여부를 따져볼 수 있기 때문이다. - 이런 코드를 명시적으로 암시적이라하는대 이런 코드는 무조건 쓰지 말자.
- 이런 코드보단
Boolean(a)
이나!!a
같은 명시적 강제변환이 훨씬 좋다.
🎯 암시적 변환
- 암시적 강제변환은 부수 효과가 명확하지 않게 숨겨진 형태로 일어나는 타입변 환이다.
📚 암시적이란?
- 엄격한 타입 언어(Strongly Typed Language)에서 말하는 이론적인 의사 코드를 살펴보자.
SomeType x = SomeType(AnotherType(y))
- 임의의 타입, y 값을
SomeType
타입으로 변환할 때 곧바로 변환할 수 없다. - 다음과 같이 직접 기술한다고하자.
SomeType x = SomeType(y)
- 코드를 이렇게 쓸 수 있다면 중간 변환 단계를 줄여 타입변환을 단순화했다고 할 수 있을까?
- 실제로 코드 가독성을 높이고 세세한 구현부를 추상화하거나 감추는 데 도움이 된다고 생각한다.
- 위와 같은 이야기의 핵심은 자바스크립트의 암시적 강제변환 역시 같은 이치로 우리가 작성하는 코딩에 도움을 줄 수 있다.
📚 암시적 강제변환: 문자열 ↔ 숫자
- 암시적 강제변환을 일으키는 몇 개의 연산들이 있다.
+
연산자는 숫자의 덧셈과 문자열 접합 두 가지 목적으로 오버로드(Overload)된다.
var a = '42';
var b = '0';
var c = 42;
var d = 0;
a + b; // '420'
c + d; // 42
- 피연산자가 한쪽 또는 양쪽 모두 문자열인지 아닌지에 따라 + 연산자가 문자열 붙이기를 할지 결정한다고 하지만, 부분적으로는 맞지만 실상은 그보다 더 복잡하다.
var a = [1,2];
var b = [3,4];
a + b; // "1,23,4"
- ES5 11.6.1에 따르면
+
알고리즘(피연산자가 객체 값일 경우)은 한쪽 연산자가 문자열이거나 다음 과정을 통해 문자열 표현형으로 나타낼 수 있으면 문자열 붙이기를 한다. - 따라서 피연산자 중 하나가 객체(배열 포함)라면, 먼저 이 값에
ToPrimitive
추상 연산을 수행하고, 다시ToPrimitive
는number
콘텍스트 힌트를 넘겨[[DefaultValue]]
알고리즘을 호출한다. valueOf()
에 배열을 넘기면 단순 원시 값을 반환할 수 없으므로toString()
으로 넘어간다.- 그래서 두 배열은
"1,2"
와"3,4"
가 되고,+
는 두 문자열을 붙여 최종 결괏값은"1,23,4"
가 된다.
var a = [1,2];
var b = [3,4];
valueOf(a); // Uncaught TypeError: Cannot convert undefined or null to object
a.toString() + b.toString(); // "1,23,4"
a+b; // "1,23,4"
+
연산의 한쪽 피연산자가 문자열이면+
는 문자열 붙이기 연산을 한다. 그 밖에는 언제나 숫자 덧셈을 한다.- 숫자는 공백 문자열
''
와 더하면 문자열로 강제변환된다.
var a = 42;
var b = a + '';
b; // '42'
- 명시적 강제변환
String(a)
에 비해 암시적 강제변환a + ""
에서는 한 가지 유의해야 할 사항이 있다. ToPrimitive
연산 과정에서a + ''
는a
값을valueOf()
메서드에 전달하여 호출하고, 그 결괏값은ToString
추상 연산을 하여 최종적인 문자열로 변환된다.- 그러나
String(a)
는toString()
를 직접 호출할 뿐이다. - 두 방법 모두 궁극적으로 변환된 문자열을 반환하지만 평범한 원시 숫자 값이 아닌 객체라면 결괏값이 달라질 수 있다.
var a = {
valueOf: function(){ return 42; },
toString: function(){ return 4; },
};
a + ''; // '42'
String(a); // '4'
- 문자열 ➡ 숫자인 암시적인 강제변환은 어떨까?
var a = '3.14';
var b = a - 0;
b; // 3.14
-
연산자는 숫자 뺄셈 기능이 전부이므로a - 0
은a
값을 숫자로 강제변환한다.- 객체 값에
-
연산을 하면 이전의+
와 비슷하다.
var a = [3];
var b = [1];
a - b; // 2
- 두 배열은 우선 문자열로 강제변환 뒤(toString()로 직렬화) 숫자로 강제변환된다.
- 그리고 마지막엔
-
연산을 한다. b = String(a)
(명시적)과b = a + ''
(암시적) 둘 다 경우에 따라 유용하게 코그에 쓰일 수 있지만 자바스크립트 프로그램에선 후자를 훨씬 더 많이 사용한다.
📚 암시적 강제변환: 불리언 ➡ 숫자
function onlyOne(a, b, c) {
return !!((a && !b && !c) || (!a && b && !c) || (!a && !b && c));
}
var a = true;
var b = false;
onlyOne(a, b, b); // true
onlyOne(b, a, b); // true
onlyOne(a, b, a); // false
onlyOne()
는 세 인자 중 정확히 하나만true/truthy
인지 아닌지를 확인하는 함수로truthy
체크 시 암시적 강제변환을 하고 최종 반환 값을 포함한 다른 부분은 명시적 강제변환을 한다.- 그런데 이런 식으로 4,5 ... 20개 인자를 처리해야 할 경우, 모든 비교 로직을 조합하여 코드를 후현한다는 게 상당히 어렵다.
- 하지만 불리언 값을 숫자(명시적으로 0 또는 1)로 변환하면 쉽게 풀린다.
function onlyOne() {
var sum = 0;
for(var i = 0; i < arguments.length; i++) {
// falsy 값은 건너뛴다.
// 0으로 취급하는 셈이다. 그러나 NaN은 피해야 한다.
if(arguments[i]) {
sum += arguments[i];
}
}
return sum == 1;
}
var a = true;
var b = false;
onlyOne(b, a); // true
onlyOne(b, a, b, b, b); // true
onlyOne(b, b); // false
onlyOne(b, a, b, b, b, a); // false
true/truthy
를 숫자로 강제변환하면 1이므로 그냥 숫자를 모두 더한 것이 전부이고,sum += arguments[i]
에서 암시적으로 강제변환이 일어난다.- 인자 중 딱 하나만
true
일 때sum
은 1이고, 그 외에는 1이 되지 않으므로 조건을 확인할 수 있다. - 다음 코드는 명시적 강제변환 버전이다.
function onlyOne() {
var sum = 0;
for(var i = 0; i < arguments.length; i++) {
sum += Number(!!arguments[i]);
}
return sum === 1;
}
!!arguments[i]
로 인자 값을true/false
로 강제변환한다.- 따라서
onlyOne('42', 0)
처럼 비 불리언 값을 넘긴다 해도 문제되지 앟는다. !!arguments[i]
는 불리언 값이 확실하므로Number()
로 한 번 더 강제변환하여 0 또는 1로 바꾼다.
📚 암시적 강제변환: *
➡ 불리언
- 다음은 불리언으로의 (암시적인) 강제변환이 일어나는 표현싱를 열거한 것이다.
if()
문의 조건 표현식for( ; ; )
에서 두번째 조건 표현식while()
및do...while()
루프의 조건 표현식?
:
삼항 연산 시 첫 번째 조건 표현식||
및&&
의 좌측 피연산자
- 이런 콘텍스트에서 불리언이 아닌 값이 사용되면, 이 장 앞부분에서 언급했던
ToBoolean
추상 연산 규칙에 따라 일단 불리언 값으로 암시적 강제변환된다.
var a = 42;
var b = 'abc';
var c;
var d = null;
if(a) {
console.log('넵'); // 넵
}
while(c) {
console.log('실행 안됨');
}
c = d ? a : b; // 'abc'
if((a && d) || c) {
console.log('실행 안됨');
}
📚 && 와 || 연산자
- 자바스크립트에서 이 두 연산자는 다른 언어와 달리 실제로 결괏값이 논리 값(불리언)이 아니다.
- 두 피연산자 중 한쪽(오직 한쪽의) 값이다. 즉, 두 피연산자의 값들 중 하나를 선택한다.
- ES5 11.11 명세
&&
또는||
연산자의 결괏값이 반드시 불리언 타입이어야 하는 것은 아니며 항상 두 피연산자 표현식 중 어느 한쪽 값으로 귀결된다.
var a = 42;
var b = 'abc';
var c = null;
a || b; // 42
a && b; // 'abc'
c || b; // 'abc'
c && b; // null
||
,&&
연산자는 우선 첫 번째 피연산자의 불리언 값을 평가한다.- 피연산자가 비 불리언 타입이라면 먼저
ToBoolean
로 강제변환 후 평가를 계속한다. ||
연산자는 그 결과가true
면 첫 번째 피연산자 값을,false
이면 두 번째 피연산자 값을 반환한다.- 이와 반대로
&&
연산자는true
이면 두 번째 피연산자의 값을,false
이면 첫 번째 피연산자의 값을 반환한다. ||
,&&
표현식의 결괏값은 언제나 피연산자의 값 중 하나이고, (필요시 강제변환된) 평가 결과가 아니다.c && b
에서c
는null
이므로falsy
값이다. 하지만&&
표현식은 평가 결과인false
가 아니라c
자신의 값,null
로 귀결된다.
a || b;
// 대략 다음과 같다.
a ? a : b;
a && b;
// 대략 다음과 같다.
a ? b : a;
||
연산자의 이러한 사용 패턴은 매우 흔하고 쓸 만하지만falsy
값은 무조건 건너뛸 경우에만 사용해야 한다.- 그렇지 않으면 조건 평가식을 삼항 연산자로 더욱 명시적으로 지정해야 한다.
&&
연산자는 첫 번째 피연산자의 평가 결과가truthy
일 때에만 두 번째 피연산자를 선택한다고 했는데 이런 특성을 가드 연산자(Guard Operator
)라고 한다.
function foo() {
console.log(a);
}
var a = 42;
a && foo(); // 42
- 먼저 복합 표현식이 평가된 다음 불리언으로 암시적 강제변환이 일어난다.
var a = 42;
var b = null;
var c = 'foo';
if(a && (b || c)) {
console.log('넵');
}
- 위 코드는
a && (b || c)
표현식의 실제 결과는true
가 아닌"foo"
이다.if()
문은 이"foo"
를 불리언 타입으로 강제변환하여true
로 만든다.
📚 심벌의 강제변환
- ES6부터 새로 등장한 심벌 탓에 강제변환 체계에 조심해야 할 함정이 하나 더 늘어났다.
- 심벌 ➡ 문자열 명시적 강제변환은 허용되나 암시적 강제변환은 금지되며 시도만 해도 에러가 난다.
var s1 = Symbol('좋아');
String(s1); // "Symbol(좋아)"
var s2 = Symbol('구려');
s2 + ''; // Uncaught TypeError: Cannot convert a Symbol value to a string
- 심벌 값은 절대 숫자로 변환되지 않지만(양방향 모두 에러), 불리언 값으로는 명시적/암시적 모두 강제변환(항상 true)이 가능하다.
🎯 느슨한/엄격한 동등 비교
- 느 슨한 동등 비교(Loose Equals)는
==
연산자를, 엄격한 동등 비교(Strict Equals)는===
연산자를 각각 사용한다. - 동등함을 비교 시
==
는 강제변환을 허용하지만,===
는 강제변환을 허용하지 않는다.
📚 비교 성능
- 타입이 같은 두 값의 동등 비교라면,
==
와===
의 알고리즘은 동일하다. - 엔진의 내부 구현 방식은 조금씩 다를 수도 있지만, 기본적으로 하는 일은 같다.
==
와===
는 성능은 불과 몇 마이크로 초 단위의 차이일 뿐이기 때문에 영향을 미치지는 않는다.- 강제변환이 필요하다면 느슨한 동등 연산자(
==
)를, 필요하지 않다면 엄격한 동등 연산자(===
)를 사용한다.
📚 추상 동등 비교
==
연산자 로직은 추상적 동등 비교 알고리즘에 상술되어 있다.- 첫째 항(11.9.3.1)에는 비교할 두 값이 같은 타입이면 값을 식별하여 간단히, 자연스럽게 견주어본다.
- 42와 동등한 값은 42,
"abc"
와 동등한 값은"abc"
뿐이다. - 다음 예외는 사소하나마 상식을 벗어나므로 주의해야 한다.
NaN
은 그 자신과도 결코 동등하지 않다.- +0와 -0는 동등하지 않다.
- 마지막 항목에서는 객체의 느슨한 동등 비교에 대해 두 객체가 정확히 똑같은 값에 대한 레퍼런스일 경우에만 동등하다고 기술되어 있다.
- 여기서 강제변환은 일어나지 않는다.
- 객체의 동등 비교에 있어서
==
와===
의 로직이 똑같다는 사실은 거의 알려져 있지 않다.
📌 비교하기: 문자열 ➡ 숫자
var a = 42;
var b = '42';
a === b; // false
a == b; // true
- 느슨한 동등 비교
a == b
에서는 피연산자의 타입이 다르면, 비교 알고리즘에 의해 한쪽 또는 양쪽 피연산자 값이 알아서 암시적으로 강제변환된다.
- ES5 11.9.3.4-5 원문 참고
Type(x)
가 Number고Type(y)
가 String이면,x == ToNumber(y)
비교 결과를 반환한다.Type(x)
가 String이고Type(y)
가 Number면,ToNumber(x) == y
비교 결과를 반환한다.
📌 비교하기: *
➡ 불리언
var a = '42';
var b = true;
a == b; // false
'42'
는truthy
값이니==
비교하면true
일거 같지만 그렇지 않다.
- ES5 11.9.3.6-7
Type(x)
이 불리언이면ToNumber(x) == y
의 비교 결과를 반환한다.Type(y)
이 불리언이면x == ToNumber(y)
의 비교 결과를 반환한다.
var x = true;
var y = '42';
x == y; // false
Type(x)
은 불리언이므로ToNumber(x) ➡ 1
로 강제변환된다. 따라서1 == '42'
이 되는데 타입이 상이하므로 (재귀적으로) 알고리즘을 수행한다.- 결국
'42'
는 42로 바뀌어1 == 42
는false
가 된다. - 순서를 바꾸어도 결과는 같다.
"42"
는 분명truthy
값이지만"42" == true
는 불리언 평가나 강제변환을 전혀 하지 않는다."42"
가 불리언으로 강제변환되는 것이 아니라, 도리어true
가 1로 강제변환되고 그 후"42"
가 42로 강제변횐된다.- 어쨌든
ToBoolean
은 전혀 개입하지 않으며,"42"
값 자체의truthy/falsy
여부는==
연산과는 전혀 무관하다. ==
피연산자 한쪽이 불리언 값이면 예외 없이 그 값이 먼저 숫자로 강제변환된다.- 그렇기 때문에
== true
,== false
같은 코드는 쓰지 말자. === true
,=== false
는 강제변환을 허용하지 않기에ToNumber
강제변환 따위는 신경 쓰지 않아도 된다.
var a = '42';
// No
if (a == true) {
// ...
}
// No
if (a === true) {
// ...
}
// 암시적으로 작동
if(a) {
// ...
}
// 명시적으로 작동
if(!!a) {
// ...
}
// 명시적으로 작동
if(Boolean(a)) {
// ...
}
📌 비교하기: null ➡ undefined
null
과undefined
간의 변환은 느슨한 동등 비교==
이 암시적 강제변환을 하는 또 다른 예다.
- ES5 11.9.3.2-3 인용
- x가
null
이고 y가undefined
면true
를 반환한다.- x가
undefined
이고 y가null
이면true
를 반환한다.
null
과undefined
를 느슨한 동등 비교를 하면 서로에게 타입을 맞춘다.(강제변환한다.)- 즉,
null
과undefined
는 느슨한 동등 비교 시 상호 간의 암시적인 강제변환이 일어나므로 비교 관점에서 구분이 되지 않는 값으로 취급되는 것이다.
var a = null;
var b;
a == b; // true
a == null; // true
b == null; // true
a == false; // false
b == false; // false
a == ''; // false
b == ''; // false
a == 0; // false
b == 0; // false
null
과undefined
강제변환은 안전하고 예측 가능하며, 어떤 다른 값도 비교 결과 긍정 오류(False Positive) 을 할 가능성이 없다. 즉,null
과undefined
자신들끼리 비교 결과가true
이므로, 이 외의 값들과 비교했을 때 결괏값이true
일 가능성이 없다.
var a = doSomething();
if(a == null) {
// ...
}
a == null
의 평과 결과는doSomething()
이null
이나undefined
를 반환할 경우에만true
, 이외의 값이 반환되면(0,false
,''
등falsy
한 값이 넘어와도)false
이다.
📌 비교하기: 객체 ➡ 비객체
- 객체/함수/배열과 단순 스칼라 원시 값(문자열, 숫자, 불리언)의 비교는 ES5 11.9.3.8-9를 참고
- ES5 11.9.3.8-9
Type(x)
가 String 또는 Number고Type(y)
가 객체라면,x == ToPrimitive(y)
의 비교 결과를 반환한다.Type(x)
가 Object이고Type(y)
가 String 또는 Number라면,ToPrimitive(x) == y
의 비교 결과를 반환한다.
var a = 42;
var b = [42];
a == b; // true
[42]
는 ToPrimitive 추상 연산 결과,"42"
가 된다. 그리고"42" == 42
는42 == 42
이므로 a, b는 동등하다.
var a = 'abc';
var b = Object(a); // new String(a)와 같다.
a === b; // false
a == b; // true
- b는 ToPrimitive 연산으로
"abc"
라는 단순 스칼라 원시 값으로 강제변환되고, 이 값은 a와 동일하므로a == b
는true
이다. - 하지만 항상 그런 것은 아니고,
==
알고리즘에서 더 우선하는 규칙 때문에 그렇지 않는 경우들도 있다.
var a = null;
var b = Object(a); // Object()와 같다.
a == b; // false
var c = undefined;
var d = Object(c); // Object()와 같다.
c == d; // false
var e = NaN;
var f = Object(e); // new Number(e)와 같다.
e == f; // false
null
과undefined
는 객체 래퍼가 따로 없으므로 박싱할 수 없다.- 그래서
Object(null)
는Object()
로 해석되어 그냥 일반 객체가 만들어진다. NaN
은 해당 객체 래퍼인Number
로 박싱되지만,==
를 만나 언박싱되면 결국 조건식은NaN == NaN
이 되어 (NaN
은 자기 자신과도 같지 않다.) 결과는false
이다.
📚 희귀 사례 (Corner Cases)
- 먼저 내장 네이티브 프로토타입을 변경하면 어떤 일이 일어날까?
📌 알 박힌 숫자 값
Number.prototype.valueOf = function() {
return 3;
};
new Number(2) == 3; // true
2 == 3
비교는 이 예와 무관하다.- 2, 3이 둘 다 이미 원시 숫자 값이고 곧바로 비교가 가능하므로
Number.prototype.valueOf()
내장 메서드는 호출되지 않는다. - 그러나
new Number(2)
는 무조건ToPrimitive
강제변환 후valueOf()
를 호출한다.
if (a == 2 && a == 3) {
// ...
}
- a가 동시에 2가 되고 3이 돤다는 게 말이 되나 싶겠지만, 동시에란 전제부터가 틀렸다.
- 엄밀히 말해, 두 표현식 중
a == 2
가a == 3
보다 먼저 평가된다. a.valueOf()
에 부수 효과를 주면? 이를 테면 처음 호출하면 2, 두 번째 호출하면 3을 반환하는 식.
var i = 2;
Number.prototype.valueOf = function() {
return i++;
};
var a = new Number(42);
if(a == 2 && a == 3) {
console.log('흠...');
}
- 이런 코드는 자체로 공해니 생각조차 말고 강제변환을 비방하는 근거로 제시하지도 말자.
📌 Falsy 비교
'0' == null; // false
'0' == undefined; // false
'0' == false; // true ?
'0' == NaN; // false
'0' == 0; // true
'0' == ''; // false
false == null; // false
false == undefined; // false
false == NaN; // false
false == 0; // true ?
false == ''; // true ?
false == []; // true ?
false == {}; // false
'' == null; // false
'' == undefined; // false
'' == NaN; // false
'' == 0; // true ?
'' == []; // true ?
'' == {}; // false
0 == null; // false
0 == undefined; // false
0 == NaN; // false
0 == []; // true ?
0 == {}; // false
""
와NaN
은 전혀 동등할 만한 값들이 아니며 실제로도 느슨한 동등 비교 시 강제변환되지 않는다."0"
과 0은 그냥 봐도 같은 값이며 느슨한 동등 비교 시 강제변환된다.- 여기서
?
의 7개의 비교는 긍정 오류(false positive)이다. ""
와 0은 분명히 다른 값이며 같은 값으로 취급할 경우 또한 거의 없기 때문에 상호 강제변환은 문제가 있다. 7개 중 부정 오류는 하나도 없다.
📌 말도 안 되는...
- 더 심각한 강제변환 사례도 있다.
[] == ![]; // true
!
단항 연산자는ToBoolean
으로 불리언 값으로 명시적 강제변환을 하는 연산자이다.- 따라서
[] == ![]
이전에 이미[] == false
로 바뀐다.
2 == [2]; // true
'' == [null]; // true
- 우변의
[2]
,[null]
은 ToPrimitive가 강제변환을 하여 좌변과 비교 가능한 원시 값(2, "")으로 바꾼다. - 배열의
valueOf()
메서드는 배열 자신을 반환하므로 강제변환 시 배열을 문자열화한다. String(null)
이"null"
이니String([null])
역시"null"
이어야 맞다고 생각할 수 있다.- 여기서 암시적 변환은 그 자체로도 문제가 없다.
[null]
을 명시적 강제변환을 해도 결과는""
이다.- 배열을 그 내용과 동등하게 문자열화하는 게 맞는 것인지, 그리고 정확히 어떤 방법으로 문자열화해야 하는지, 이 두 문제가 서로 앞뒤가 맞지 않는다.
- 따라서
String([])
규칙들이 비난의 대상이 되어야 할 것이다. - 아예 배열에서 문자열 강제변환을 없애버리는 것은 생각은 자바스크립트 언어의 다른 쪽에서 많은 흠집이 생기게 된다.
0 == '\n'; // true
- 공백 문자
" "
,"\n"
이 ToNumber를 경유하여 0으로 강제변환된다. (Number("\n")
=> 0) ""
또는" "
를 숫자로 강제변환한 결괏값으로 차선책을 찾느다면NaN
을 떠올릴 수 있지만NaN
이 0보다 더 나은 대안이 될 수 없다." " == NaN
는 당연히false
이지만 모든 걱정거리가 다 해결될 수 있을지는 확실치 않다.
📌 근본부터 따져보자
- 예상에 벗어난 정도를 기준으로 하면 강제변환이 심각한 문제가 될 경우는 7가지이다.
- 하지만 나머지 항목들은 강제변환은 이치에 맞고 설명이 가능하다.
// 나쁜 부분 7가지
'0' == false; // true ?
false == 0; // true ?
false == ''; // true ?
false == []; // true ?
'' == 0; // true ?
'' == []; // true ?
0 == []; // true ?
- 이들 중 처음 4개는
== false
비교와 연관되며, 다시 말하지만 이런 의미 없는 비교는 절대 하지 말자. - 나머지 3가지는 실제로 이런 강제변환 코드를 쓸 일이 있을까?
- 실제로
== []
이런식의 불리언 평가를 할 경우는 극히 드물다. - 대신
== ""
와== 0
는 사용하지 않을까?
function doSomething(a) {
if(a == ''){
// ...
}
}
- 다음 예제를 확인하자
function doSomething(a, b) {
if(a == b) {
// ...
}
}
- 이 역시
doSomething("", 0)
이나doSomething([], '')
로 호출하면 문제가 된다. - 결국, 강제변환 때문에 골탕 먹을 경우의 수가 있다는 사실은 부인하기 어렵고 함정에 빠지지 않으려면 주의할 필요는 있지만, 그럴 만한 코드가 나올 가능성은 매우 희박하다.
📌 암시적 강제변환의 안전한 사용법
- 불행의 사고를 미연에 방지하기 위해서 동등 비교 원칙이다.
- 피연산자 중 하나가
true/false
일 가능성이 있으면 절대로==
연산자를 쓰지 말자. - 피연산자 중 하나가
[]
,""
, 0 이 될 가능성이 있으면 가급적==
연산자는 쓰지 말자.
- 피연산자 중 하나가
- 이런 상황이라면
==
대신===
를 사용하여 의도하지 않은 강제변환을 차단하는게 훨씬 좋다. - 결국
==
냐===
냐 하는 문제는 한 마디로 동등 비교 시 강제변환을 허용할 거냐 말 거냐와 본질적으로 같다. - 비교 로직을 (이를테면
null
과undefined
와 함께 사용하여) 간결하게 표현할 수 있는 강제변환이 유용한 경우가 많다. - 전반적으로 암시적 강제변환이 정말로 위험한 경우는 그다지 흔치 않다. 하지만 이런 경우라고 해도
===
로 대체하여 안전하게 가면 된다. - 강제변환에 상처받지 않을 또 한 가지 비책은 바로
typeof
연산자다. typeof
연산은 항상 7가지 문자열 중 하나, 값의 타입을 체크한다고 암시적 강제변환과 문제가 될 일은 전혀 없다.typeof x == 'function'
,typeof x === 'function'
둘 다 100% 안전하다.- 동등 비교의 모든 조합을 나타낸 테이블 참고
🎯 추상 관계 비교
- ES5 11.8.5 추상적 관계 비교(Abstract Relational Comparison) 알고리즘은 비교 시 피연산자 모두 문자열(후반부)일 때와 그 외의 경우(전반부), 두 가지로 나뉜다.
명세에는 a < b 에 대해서만 정의되어 있다. a > b는 b < a로 처리된다.
- 이 알고리즘은 먼저 두 피연산자에 대해 ToPrimitive 강제변환을 실시하는 것으로 시작한다.
- 그 결과, 어느 한쪽이라도 문자열이 아닐 경우 양쪽 모두 ToNumber로 강제변환하여 숫자값으로 만들어 비교한다.
var a = [42];
var b = ["43"];
a < b; // true
b < a; // false
-0과 NaN 등을 조심해야 하는 건 앞서 설명한
==
알고리즘과 비슷하다.
- 그러나
<
비교 대상이 모두 문자열 값이면, 각 문자를 단순 어휘(알파벳 순서로) 비교 한다.
var a = ['42'];
var b = ['043'];
a < b; // false
- 두 배열을 ToPrimitive로 강제변환하면 문자열이기 때문에 a, b는 숫자로 강제변환하지 않는다.
- 따라서 문자 단위로(우선 "4"와 "0"을 비교하고, 그 다음 "2"와 "4"를 비교하는 방식) 비교한다.
"0"
은 어휘상"4"
보다 작은 값이므로 비교는 처음부터 실패한다.- 다음 코드 역시 수행 로직은 동일하다.
var a = [4, 2];
var b = [0, 4, 3];
a < b; // false
- a는
"4, 2"
로, b는"0, 4, 3"
으로 문자열화시킨 후 앞 예제와 마찬가지로 어휘 비교를 한다.
var a = { b: 42 };
var b = { b: 43 };
a < b; // false
- a도
[object Object]
, b도[object Object]
로 변환되어 어휘적인 비교를 할 수 없기 때문이다. - 하지만 이상한 건,
var a = { b: 42 };
var b = { b: 43 };
a < b; // false
a == b; // false
a > b; // false
a <= b; // true
a >= b; // true
a == b
는 둘 다 동일한 문자열([object Object]
)이면 동등한 것일 거라 생각하겠지만==
이 객체 레퍼런스에서 두 객체가 정확히 똑같은 값에 대한 레퍼런스일 경우에만 동등하다. 그렇기 때문에 a, b는 아예 값 자체도 다른 별개의 객체이다.a <= b
와a >= b
는 왜true
일까?a <= b
는 실제로b < a
의 평가 결과를 부정하도록 명세에 기술되어 있기 때문이다.- 그래서
b < a
가false
이므로a <= b
는 이를 부정한true
가 된다. - 실제로 자바스크립트 엔진은
<=
를 더 크지 않은 (!(a > b)
➡!(b < a)
로 처리)의 의미로 해석한다. - 더구나
a >= b
는 먼저b <= a
로 재해석한 다음 동일한 추론을 적용한다. - 불행히도 동등 비교에 관한 한 엄격한 관계 비교는 없다.
- 다시 말해, 비교 전 a와 b 모두 명시적으로 동일한 타입임을 확실히 하는 방법 말고 a < b 같은 관계 비교 과정에서 암시적 강제변환을 원천 봉쇄할 수는 없다.
42 < "43"
처럼 강제변환이 유용하고 어느 정도 안전한 관계 비교라면 그냥 쓰자.- 반면, 조심해서 관계 비교를 해야할 것 같은 상황에서는
<
(또는>
)를 사용하기 전, 비교할 값들을 명시적으로 강제변환해 두는 편이 안전하다.
var a = [42];
var b = '043';
a < b; // false
Number(a) < Number(b); // true
🎯 정리하기
- 이 장에서는 강제변환이라고 하는 자바스크립트의 타입변환의 작동 원리를 명시적/암시적 두가지 유형으로 나누어 알아보았다.
- 강제변환은 많은 욕을 먹고 애물단지지만 알고 보면 꽤 유용한 기능.
- 명시적 강제변환은 다른 타입의 값으로 변환하는 의도가 확실한 코드를 말하며 혼동의 여지를 줄이고 코드 가독성 및 유지 보수성을 높일 수 있는 장점이 있다.
- 암시적 강제변환은 숨겨진 로직에 의한 부수 효과가 있으며 타입변환이 처리되는 과정이 명확하지 않다.
- 그래서 암시적 강제변환이 명시적 강제변환이 정반대이고 나쁜 것이라고들 하지만 암시적 강제변환이 오히려 코드 가독성을 향상시키는 장점도 있다.
- 암시적 강제변환은 변환 과정이 구체적으로 어떻게 일어나는지 명확하게 알고 사용해야 한다.