본문으로 건너뛰기

🌈 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 문자열화

  • ToStringJSON.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]}"
  • 함수인 대체자는 최초 호출 시 키 인자 kundefined이다.
  • 대체자는 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이 되고, undefinedNaN으로, 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가 아닌 값을 불리언에 상당한 값으로 강제변환했을 때, 이 값들은?
  • 자바스크립트 모든 값은 다음 둘 중 하나다.
    1. 불리언으로 강제변환하면 false가 되는 값
    2. 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에 해당한다. 두 번째 n0-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 추상 연산을 수행하고, 다시 ToPrimitivenumber 콘텍스트 힌트를 넘겨 [[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 - 0a 값을 숫자로 강제변환한다.
  • 객체 값에 -연산을 하면 이전의 +와 비슷하다.
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로 바꾼다.

📚 암시적 강제변환: * ➡ 불리언

  • 다음은 불리언으로의 (암시적인) 강제변환이 일어나는 표현싱를 열거한 것이다.
    1. if() 문의 조건 표현식
    2. for( ; ; )에서 두번째 조건 표현식
    3. while()do...while() 루프의 조건 표현식
    4. ? : 삼항 연산 시 첫 번째 조건 표현식
    5. ||&&의 좌측 피연산자
  • 이런 콘텍스트에서 불리언이 아닌 값이 사용되면, 이 장 앞부분에서 언급했던 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 에서 cnull이므로 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 원문 참고
    1. Type(x)가 Number고 Type(y)가 String이면, x == ToNumber(y) 비교 결과를 반환한다.
    2. 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
    1. Type(x)이 불리언이면 ToNumber(x) == y의 비교 결과를 반환한다.
    2. Type(y)이 불리언이면 x == ToNumber(y)의 비교 결과를 반환한다.
var x = true;
var y = '42';

x == y; // false
  • Type(x)은 불리언이므로 ToNumber(x) ➡ 1로 강제변환된다. 따라서 1 == '42'이 되는데 타입이 상이하므로 (재귀적으로) 알고리즘을 수행한다.
  • 결국 '42'는 42로 바뀌어 1 == 42false가 된다.
  • 순서를 바꾸어도 결과는 같다.
  • "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

  • nullundefined 간의 변환은 느슨한 동등 비교 ==이 암시적 강제변환을 하는 또 다른 예다.
  • ES5 11.9.3.2-3 인용
    1. x가 null이고 y가 undefinedtrue를 반환한다.
    2. x가 undefined이고 y가 null이면 true를 반환한다.
  • nullundefined느슨한 동등 비교를 하면 서로에게 타입을 맞춘다.(강제변환한다.)
  • 즉, nullundefined는 느슨한 동등 비교 시 상호 간의 암시적인 강제변환이 일어나므로 비교 관점에서 구분이 되지 않는 값으로 취급되는 것이다.
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
  • nullundefined 강제변환은 안전하고 예측 가능하며, 어떤 다른 값도 비교 결과 긍정 오류(False Positive) 을 할 가능성이 없다. 즉, nullundefined 자신들끼리 비교 결과가 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
    1. Type(x)가 String 또는 Number고 Type(y)가 객체라면, x == ToPrimitive(y)의 비교 결과를 반환한다.
    2. Type(x)가 Object이고 Type(y)가 String 또는 Number라면, ToPrimitive(x) == y의 비교 결과를 반환한다.
var a = 42;
var b = [42];

a == b; // true
  • [42]는 ToPrimitive 추상 연산 결과, "42"가 된다. 그리고 "42" == 4242 == 42이므로 a, b는 동등하다.
var a = 'abc';
var b = Object(a); // new String(a)와 같다.

a === b; // false
a == b; // true
  • b는 ToPrimitive 연산으로 "abc"라는 단순 스칼라 원시 값으로 강제변환되고, 이 값은 a와 동일하므로 a == btrue이다.
  • 하지만 항상 그런 것은 아니고, == 알고리즘에서 더 우선하는 규칙 때문에 그렇지 않는 경우들도 있다.
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
  • nullundefined객체 래퍼가 따로 없으므로 박싱할 수 없다.
  • 그래서 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 == 2a == 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 이 될 가능성이 있으면 가급적 == 연산자는 쓰지 말자.
  • 이런 상황이라면 == 대신 ===를 사용하여 의도하지 않은 강제변환을 차단하는게 훨씬 좋다.
  • 결국 =====냐 하는 문제는 한 마디로 동등 비교 시 강제변환을 허용할 거냐 말 거냐와 본질적으로 같다.
  • 비교 로직을 (이를테면 nullundefined와 함께 사용하여) 간결하게 표현할 수 있는 강제변환이 유용한 경우가 많다.
  • 전반적으로 암시적 강제변환이 정말로 위험한 경우는 그다지 흔치 않다. 하지만 이런 경우라고 해도 ===로 대체하여 안전하게 가면 된다.
  • 강제변환에 상처받지 않을 또 한 가지 비책은 바로 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 <= ba >= b는 왜 true일까?
  • a <= b는 실제로 b < a평가 결과를 부정하도록 명세에 기술되어 있기 때문이다.
  • 그래서 b < afalse이므로 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

🎯 정리하기

  • 이 장에서는 강제변환이라고 하는 자바스크립트의 타입변환의 작동 원리를 명시적/암시적 두가지 유형으로 나누어 알아보았다.
  • 강제변환은 많은 욕을 먹고 애물단지지만 알고 보면 꽤 유용한 기능.
  • 명시적 강제변환은 다른 타입의 값으로 변환하는 의도가 확실한 코드를 말하며 혼동의 여지를 줄이고 코드 가독성 및 유지 보수성을 높일 수 있는 장점이 있다.
  • 암시적 강제변환은 숨겨진 로직에 의한 부수 효과가 있으며 타입변환이 처리되는 과정이 명확하지 않다.
  • 그래서 암시적 강제변환이 명시적 강제변환이 정반대이고 나쁜 것이라고들 하지만 암시적 강제변환이 오히려 코드 가독성을 향상시키는 장점도 있다.
  • 암시적 강제변환은 변환 과정이 구체적으로 어떻게 일어나는지 명확하게 알고 사용해야 한다.