객체를 원시형으로 변환하기 파트: https://ko.javascript.info/object-toprimitive
객체끼리 더하거나 빼는 산술 연산을 하거나 alert 메서드로 객체를 출력하면 무슨 일이 발생할까? => 자동 형 변환이 일어난다.
객체가 원시값으로 변환되고, 그 후 의도한 연산이 수행된다.
2. 숫자형으로의 형 변환은 객체끼리 뺄셈 연산을 하거나 수학 관련 함수를 적용할 때 일어난다. Date 객체끼리 차감하면(date1 - date2) 두 날짜의 시간 차이가 반환된다.
특수 객체 메서드를 사용하면 숫자형, 문자형으로의 형 변환을 원하는대로 수행할 수 있다.
객체 형 변환은 세 종류로 구분되는데, 'hint’라 불리는 값이 구분 기준이 된다. 'hint’가 무엇인지는 명세서(https://tc39.es/ecma262/#sec-toprimitive)에 자세히 설명되어 있는데, 형 변환 시 ‘목표로 하는 자료형’ 정도로 볼 수 있다.
string
alert 함수같이 문자열을 기대하는 연산을 수행할 때는(객체-문자형 변환), hint가 string이 된다.
number
수학 연산을 적용하려 할 때(객체-숫자형 변환), hint는 number가 된다.
default
연산자가 기대하는 자료형이 ‘확실치 않을 때’ hint는 default가 된다.
예를들어 이항 덧셈 연산자 +는 피연산자의 자료형에 따라 문자열을 합치는 연산을 할 수도 있고 숫자를 더해주는 연산을 할 수도 있다. 따라서 +의 인수가 객체일때 hint는 default가 된다. 문자가 될지 숫자가 될지 모르니까.
동등 연산자 ==를 사용해 객체-문자형, 객체-숫자형, 객체-심볼형끼리 비교할 때도, 객체를 어떤 자료형으로 바꿔야 할지 확실하지 않아 hint는 default가 된다.
<script>
// 이항 덧셈 연산은 hint로 `default`를 사용
let total = obj1 + obj2;
// obj == number 연산은 hint로 `default`를 사용
if (user == 1) { ... };
</script>
연산자 <, > 역시 피연산자에 문자형과 숫자형 둘 다를 허용하는데, 이 연산자들은 hint가 'number’로 고정돼있다. 이는 하위 호환성 때문에 정해진 규칙이다.
사실 Date 객체를 제외한 모든 내장 객체는 hint가 default일 때와 number일 때를 동일하게 처리한다.
(모든 객체는 그냥 true로 평가되기 때문에 hint에 boolean형은 없다.)
객체에 objSymbol.toPrimitive메서드가 있는지 찾고, 있다면 메서드를 호출. Symbol.toPrimitive는 시스템 심볼로, 심볼형 키로 사용된다.
1번에 해당하지 않고 hint가 "string"이라면,
obj.toString()이나 obj.valueOf()를 호출한다(둘 중 존재하는 메서드만 실행됨).
1과 2에 해당하지 않고, hint가 "number"나 "default"라면
obj.valueOf()나 obj.toString()을 호출합니다(둘 중 존재하는 메서드만 실행됨).
string이나 number나 default나 다 obj.toString() 아니면 obj.valueOf() 호출한다는 건데 뭐가 다르다는 건지...
objSymbol.toPrimitive메서드 확인 => hint가 string인지 확인 => hint가 number인지 default 인지 확인. 아마 이 세 단계가 핵심이지 않을까 싶다.
자바스크립트엔 Symbol.toPrimitive라는 내장 심볼이 존재하는데, 이 심볼은 아래와 같이 목표로 하는 자료형(hint)을 명명하는 데 사용된다.
objSymbol.toPrimitive 형태로 사용된다.
<script>
obj[Symbol.toPrimitive] = function(hint) {
// 반드시 원시값을 반환해야 한다.
// hint는 "string", "number", "default" 중 하나가 될 수 있다.
};
//------------------실제 동작 예시------------------
let user = {
name: "John",
money: 1000,
//심볼을 프로퍼티로 쓸 때는 대괄호를 사용한다.
[Symbol.toPrimitive](hint) {
alert(`hint: ${hint}`);
//형변환 목표 자료형이 string이면 name을 아니면 money를 반환
return hint == "string" ? `{name: "${this.name}"}` : this.money;
}
};
// 데모:
alert(user); // hint: string -> {name: "John"}
alert(+user); // hint: number -> 1000
alert(user + 500); // hint: default -> 1500, 문자형이 될지 숫자가 될지
//모르니까 default.
</script>
이렇게 메서드를 구현하면 user는 hint에 따라 문자열(name)로 변환되기도 하고 숫자(money)로 변환되기도 한다.
이처럼 user[Symbol.toPrimitive]를 사용하면 메서드 하나로 모든 종류의 형 변환을 다룰 수 있다.
객체에 Symbol.toPrimitive가 없으면 자바스크립트는 아래 규칙에 따라 직접 형변환이 가능한 toString이나 valueOf를 호출한다.
toString과 valueOf 메서드는 반드시 원시값을 반환해야한다.
이 둘이 객체를 반환하면 그 결과는 무시되어 메서드가 처음부터 없었던 것처럼 된다.
반환 값이 객체일 경우 아래와 같이 toString, valueOf에 적용되는 규칙을 따른다.
<script>
let user = {name: "John"};
alert(user);
// [object Object] 반환. alert 함수가 대상을 string으로 형변환하니까.
alert(user.valueOf() === user);
// true, valueOf는 반환값이 객체일 때 자기 자신을 반환하니까.
</script>
위처럼 toString과 valueOf는 반환값이 객체여도 에러는 발생하지 않고 원하는 것과 다른 형태로 [object Object]를 뱉거나 객체 자기 자신을 뱉지만 Symbol.toPrimitive는 무조건 원시자료를 반환해야한다. 그렇지 않으면 에러가 발생한다!
이 메서드들을 사용하여 위에서 본 Symbol.toPrimitive 를 사용한 예제와 같은 결과를 만들어낼 수 있다.
<script>
let user = {
name: "John",
money: 1000,
// hint가 "string"인 경우
toString() {
return `{name: "${this.name}"}`;
},
// hint가 "number"나 "default"인 경우
valueOf() {
return this.money;
}
};
alert(user); // toString -> {name: "John"}
alert(+user); // valueOf -> 1000
alert(user + 500); // valueOf -> 1500
</script>
간혹 모든 형 변환을 한 곳에서(한 메서드에서) 처리해야 하는 경우가 생기면? 아래와 같이 toString만 구현해주면 된다.
객체에 Symbol.toPrimitive와 valueOf가 없으면 toString이 모든 형 변환을 처리하기 때문이다!
<script>
let user = {
name: "John",
toString() {
return this.name;
}
};
alert(user); // toString -> John
alert(user + 500); // toString -> John500
</script>
하지만 사실 위에서 소개된 Symbol.toPrimitive, toString, valueOf 이 세 메서드는 hint에 명시된 자료형으로 형 변환 되는 것을 보장하지 않는다.
toString()이 항상 문자열을 반환하리라는 보장이 없고, Symbol.toPrimitive의 hint가 "number"일 때 항상 숫자형 자료가 반환되리라는 보장이 없다.
이 메서드들이 보장하는 것은 객체가 아닌 원시값을 반환한다는 것 한가지다.
상당수의 연산자와 함수가 피연산자의 형을 변환시킨다. (ex. *는 피연산자를 숫자형으로 변환시킨다)
객체가 피연산자면 객체는 원시형으로 변환되고, 원시값이 원하는 자료형이 아닌 경우 다시 형변환이 일어난다.
<script>
let obj = {
// 다른 메서드가 없으면 toString에서 모든 형 변환을 처리.
toString() {
return "2";
}
};
alert(obj * 2);
// 4, 객체가 문자열 "2"를 반환, 곱셈 연산 과정에서 문자열 "2"가 숫자 2로 변경된다.
</script>
위는 곱셈이라서 문자 2를 숫자 2로 변환하는 과정이 발생했지만 덧셈 연산자는 문자열끼리도 더할 수 있기 때문에 변환이 좀 다르다.
<script>
let obj = {
toString() {
return "2";
}
};
alert(obj + 2);
// 22("2" + 2), 문자열이 반환되기 때문에 문자열끼리의 병합.
</script>