# Chapter09. 타입 변환과 단축 평가
# 1. 타입 변환이란?
- 타입 변환의 종류
종류 | 설명 |
---|---|
명시적 타입 변환(explicit coercion), 타입 캐스팅(type casting) | 개발자가 의도적으로 값의 타입을 변환하는 것 |
암묵적 타입 변환(implicit coercion), 타입 강제 변환(type coercion) | 개발자의 의도와는 상관없이 표현식을 평가하는 도중에 자바스크립트 엔진에 의해 암묵적으로 타입이 자동 변환하는 것 |
// 명시적 타입 변환
var x = 10;
var str = x.toString();
console.log(typeof str, str); // 'string' '10'
console.log(typeof x, x); // 'number' 10
// 암묵적 타입 변환
var y = 20;
var str = y + '';
console.log(typeof str, str); // 'string' '20'
console.log(typeof y, y); // 'number' 20
- 위 예시에서 명시적 타입 변환, 암묵적 타입 변환 모두 기존 원시 값을 직접 변경하는 것은 아니다.
- 왜냐하면 원시 값은 변경 불가능한 값이기 때문이다.
- 즉, 타입 변환은 기존 원시 값을 사용해 다른 타입의 새로운 원시 값을 생성하는 것이고 그 새로운 값을 원래 변수에 재할당하지 않는다.
# ➕ 참조 타입 중 하나인 배열을 정렬할 때는 원본 데이터가 변할 수도 있다.
타입 변환과는 약간 동떨어진 내용일 수 있으나 타입 변환 중 변수에 재할당하지 않는 부분과 관련지어 함께 생각해보면 좋은 내용일 것 같아서 작성해보았다.
- 원시 값을 타입 변환하게 되면 새롭게 생성된 원시 값을 원래 변수에 재할당하지 않는다.
- 하지만 아래 예시처럼 배열을 정렬하는
sort
메서드를 수행하면 원본 배열이 정렬된 상태로 변하게 된다.
const arr = [3, 4, 2, 5];
const sortedArr = arr.sort();
console.log(arr); // [2, 3, 4, 5]
console.log(sortedArr); // [2, 3, 4, 5]
- 만약 원본 배열의 순서는 그대로 유지하고 싶다면 아래 예시처럼 spread operator를 이용해서 새로운 배열로 복사한 후 정렬하면된다.
const arr = [3, 4, 2, 5];
const sortedArr = [...arr].sort();
console.log(arr); // [3, 4, 2, 5]
console.log(sortedArr); // [2, 3, 4, 5]
- 자신이 작성한 코드에서 암묵적 타입 변환이 발생하는지, 발생한다면 어떤 타입의 어떤 값으로 변환되는지, 그리고 타입 변환된 값으로 표현식이 어떻게 평가될 것인지 예측 가능해야 한다.
- 만약 타입 변환 결과를 예측하지 못하거나 예측이 결과와 일치하지 않는다면 오류를 생산할 가능성이 높아진다.
- 명시적 타입 변환, 암묵적 타입 변환 중 어떤 것이 제일 좋다고 명확히 규정짓기는 어렵다.
- 중요한 것은 코드를 예측할 수 있어야 한다는 것이다.
- 동료가 작성한 코드를 정확히 이해할 수 있어야 하고 자신이 작성한 코드도 동료가 쉽게 이해할 수 있어야 한다.
# 2. 암묵적 타입 변환(implicit coercion)
- 자바스크립트 엔진이 표현식을 평가할 때 개발자의 의도와는 상관없이 코드의 문맥을 고려해 암묵적으로 데이터 타입을 강제 변환하는 것을 암묵적 타입 변환이라고 한다.
- 암묵적 타입 변환이 발생하면 문자열, 숫자, 불리언과 같은 원시 타입 중 하나로 타입을 자동 변환한다.
# (1) 문자열 타입으로 변환
+
연산자는 피연산자 중 하나 이상이 문자열인 경우 문자열 연결 연산자로 동작한다.- 자바스크립트 엔진은 문자열 연결 연산자 표현식을 평가하기 위해 문자열 연결 연산자의 피연산자 중에서 문자열 타입이 아닌 피연산자를 문자열 타입으로 암묵적 타입 변환한다.
1 + '2' // '12'
- 연산자의 표현식의 피연산자만이 암묵적 타입 변환의 대상이 되는 것은 아니다.
- 예를 들어, ES6에서 도입된 템플리 리터럴의 표현식 삽입은 표현식의 평가 결과를 문자열 타입으로 암묵적 타입 변환한다.
`1 + 1 = ${1 + 1}` // "1 + 1 = 2"
문자열 타입이 아닌 값을 문자열 타입으로 암묵적 타입 변환을 수행할 때 동작하는 예시
- 숫자 타입
0 + '' // '0' -0 + '' // '0' 1 + '' // '1' -1 + '' // '-1' NaN + '' // 'NaN' Infinity + '' // 'Infinity' -Infinity + '' // '-Infinity'
// 연산자 우선순위, 결합 순서에 의해 어떤 순서로 문자열 연결 연산자를 더하느냐에 따라 결과값이 달라진다. 1 + 4 + '' // '5' 1 + '' + 4 // '14' 1 + '4' + '' // 14' '1' + 4 + '' // '14'
- 불리언 타입
true + '' // 'true' false + '' // 'false' true + 1 // 2 true + '1' // 'true1'
null
,undefined
타입
null + '' // 'null' undefined + '' // 'undefined'
- 심벌 타입
(Symbol()) + '' // TypeError: Cannot convert a Symbol value to a string
- 참조 타입
// 자바스크립트에서 객체의 대부분의 암묵적 타입 변환은 '[object Object]'로 변환된다. // 모든 자바스크립트 객체는 'toString' 메서드를 상속받는다. // 상속받은 'toString' 메서드는 객체가 문자열 타입으로 변해야 할 때 쓰인다. ({}) + '' // '[object Object]' Math + '' // '[object Math]' // 배열에서 상속된 'toString' 메서드는 객체와 약간 다르게 동작한다. // 배열의 'join' 메서드를 호출한 것과 비슷한 방식으로 동작한다. [] + '' // '' [1, 2, 3, 4] + '' // '1,2,3,4' 8 + [2] // '82' (function(){}) + '' // 'function(){}' Array + '' // 'function Array() { [native code] }'
# (2) 숫자 타입으로 변환
- 산술 연산자는 숫자 값을 만드는 역할을 한다.
- 산술 연산자의 모든 피연산자는 코드 문맥상 모두 숫자 타입이어야 한다.
1 - '1' // 0
1 * '10' // 10
1 / 'one' // NaN
- 자바스크립트 엔진은 산술 연산자 표현식을 평가하기 위해 산술 연산자의 피연산자 중에서 숫자 타입이 아닌 피연산자를 숫자 타입으로 암묵적 타입 변환한다.
- 이때 피연산자를 숫자 타입을 변환할 수 없는 경우는 산술 연산을 수행할 수 없으므로 표현식의 평가 결과는
NaN
이 된다.
- 이때 피연산자를 숫자 타입을 변환할 수 없는 경우는 산술 연산을 수행할 수 없으므로 표현식의 평가 결과는
- 또한 피연산자를 숫자 타입으로 변환해야 할 문맥은 산술 연산자뿐만 아니라 비교 연산자도 해당된다.
- 비교 연산자는 피연산자의 크기를 비교하므로 모든 피연산자는 코드의 문맥상 모두 숫자 타입이어야 한다.
'1' > 0 // true
'1' < '2' // true
그리고
+
단항 연산자는 피연산자가 숫자 타입의 값이 아니면 숫자 타입의 값으로 암묵적 타입 변환을 수행하는 가장 간단한 방법이고 자주 사용된다.숫자 타입으로 암묵적 타입 변환을 수행할 때 동작하는 예시
- 문자열 타입
+'' // 0 +'0' // 0 +'1' // 1 +'string' // NaN
- 불리언 타입
+true // 1 +false // 0
null
,undefined
타입
+null // 0 +undefined // NaN
- 심벌 타입
+Symbol() // TypeError: Cannot convert a Symbol value to a number
- 참조 타입
+{} // NaN +[] // 0 +[2] // 2 +['2'] // 2 +[1, 2] // NaN +(function(){}) // NaN
빈 문자열, 빈 배열,
null
,false
는 0으로,true
는 1로 객체와 빈 배열이 아닌 배열,undefined
는 변환되지 않아NaN
이 된다는 것에 주의하자.- 숫자 타입으로 변환되는 내부 상세 로직은
3. 명시적 타입 변환(explicit coercion)
절이 끝난 후에 내부 로직을 살펴보면서 자세히 더 살펴보기로 하자.
- 숫자 타입으로 변환되는 내부 상세 로직은
# (3) 불리언 타입으로 변환
if
문이나for
문과 같은 제어문 또는 삼항 조건 연산자의 조건식은 불리언 값, 즉 논리적 참/거짓으로 평가되어야 하는 표현식이다.- 자바스크립트 엔진은 조건식의 평가 결과를 불리언 타입으로 암묵적 타입 변환한다.
- 이 때 자바스크립트 엔진은 불리언 타입이 아닌 Truthy 값(참으로 평가되는 값) 또는 Falsy 값(거짓으로 평가되는 값)으로 구분한다.
- 즉, 제어문의 조건식과 같이 불리언 값으로 평가되어야 할 문맥에서 Truthy 값은
true
로, Falsy 값은false
로 암묵적 타입 변환된다. true
로 평가되는 Truthy 값은 무수히 많으므로false
로 평가되는 Falsy 값을 알아두면 나머지는 Truthy 값으로 생각하면 된다- Falsy 값 :
false
,undefined
,null
,0
,-0
,NaN
,''
(빈 문자열)
- 즉, 제어문의 조건식과 같이 불리언 값으로 평가되어야 할 문맥에서 Truthy 값은
- 또한 논리 부정 연산자(
!
)를 이용하여 불리언 타입으로 암묵적 타입 변환을 할 수 있다.
// 전달받은 인수가 Falsy 값이면 true, Truthy 값이면 false를 반환한다.
function isFalsy(v) {
return !v;
}
[false, undefined, null, 0, NaN, ''].forEach((v) => console.log(isFalsy(v))); // 모두 true 반환
- 주의해야할 점은 문자열 타입의 숫자
0
과 음수 값과 빈 배열([]
) 그리고 빈 객체({}
)는 Truthy 값이라는 것이다.- 보통 불리언 타입으로 변환하기 위해 아래
isTruthy
함수와 같이 논리 부정 연산자를 두 번 사용한다.
- 보통 불리언 타입으로 변환하기 위해 아래
// 전달받은 인수가 Truthy 값이면 true, Falsy 값이면 false를 반환한다.
function isTruthy(v) {
return !!v;
}
['0', -1, [], {}].forEach((v) => console.log(isTruthy(v))); // 모두 true 반환
# 3. 명시적 타입 변환(explicit coercion)
- 개발자의 의도에 따라 명시적으로 타입을 변환하는 것을 명시적 타입 변환이라고 한다.
- 그 방법으로 표준 빌트인 생성자 함수를
new
연산자 없이 호출하는 방법, 빌트인 메서드를 사용하는 방법, 암묵적 타입 변환을 이용하는 방법이 있다. - 이 때 타입 변환할 때 내부 동작은
2. 암묵적 타입 변환(implicit coercion)
절에서 살펴본 내용과 거의 동일하다. - 이번 절에서는 이전 절에서 등장하지 않은 방법에 대해서만 소개하고자 한다.
- 그 방법으로 표준 빌트인 생성자 함수를
# (1) 문자열 타입으로 변환
# 📍 String
생성자 함수를 new
연산자 없이 호출하는 방법
// 숫자 타입 => 문자열 타입
String(1); // '1'
String(NaN); // 'NaN'
String(Infinity); // 'Infinity'
// 불리언 타입 => 문자열 타입
String(true); // 'true'
String(false); // 'false'
// 참조 타입 => 문자열 타입
String([]); // ''
String([1, 2, 3, 4]); // '1,2,3,4'
String({}); // '[object Object]'
# 📍 Object.prototype.toString
메서드를 사용하는 방법
// 숫자 타입 => 문자열 타입
(1).toString(); // '1'
(NaN).toString(); // 'NaN'
(Infinity).toString(); // 'Infinity'
// 불리언 타입 => 문자열 타입
(true).toString(); // 'true'
(false).toString(); // 'false'
// 참조 타입 => 문자열 타입
([]).toString(); // ''
([1, 2, 3, 4]).toString(); // '1,2,3,4'
({}).toString(); // '[object Object]'
# (2) 숫자 타입으로 변환
# 📍 Number
생성자 함수를 new
연산자 없이 호출하는 방법
// 문자열 타입 => 숫자 타입
Number('0'); // 0
Number('-123'); // -123
Number('12.34'); // 12.34
Number('12.00'); // 12
Number('12.30'); // 12.3
Number('12.34km'); // NaN
// 불리언 타입 => 숫자 타입
Number(true); // 1
Number(false); // 0
// 참조 타입 => 숫자 타입
Number({}); // NaN
Number([]); // 0
Number([2]); // 2
Number(['2']); // 2
Number([1, 2]); // NaN
Number([Infinity]); // Infinity
# 📍 parseInt
, parseFloat
함수를 사용하는 방법
parseInt
함수는 정수 형태로,parseFloat
함수는 부동소수점 실수 형태로 반환한다.- 단, 이 방법은 문자열을 숫자 타입으로 변환할 때만 가능하다.
만약, 함수의 인자로 문자열이 아닌 다른 타입을 작성하는 경우 아래와 같이 함수 인자로
'string'
타입만 올 수 있다는 에러 메시지를 볼 수 있다.parseInt(1);
구문에서 출력되는 error 메시지 및parseInt
함수 설명parseFloat(1);
구문에서 출력되는 error 메시지 및parseFloat
함수 설명
또한 추가적으로
parseInt
함수의 경우 두 번째 인자로 몇 진법으로 표시할지 결정하는radix
값을 넘길 수 있다.radix
인자는?
에 의해 optional로 넣어도 되고 안 넣어도 무방하지만 eslint의 기본 설정에서는 인자를 넘기라고 되어 있다.- 아무 값도 넘기지 않으면 기본값은
10
이 되어 십진법으로 반환된다. - 만약 이 eslint 옵션을 해당 line에만 무효화시키고 싶은 경우 아래와 같이 eslint 전용 주석을 그 위에 추가하면 된다.
// eslint-disable-next-line radix parseInt(1);
parseInt
함수의radix
인자에 대한 자세한 설명은 MDN 공식 문서 (opens new window)를 참고하자.
parseInt('0'); // 0
parseInt('-123'); // -123
// parseInt 함수는 정수를 반환한다는 점 주의하자.
parseInt('12.34'); // 12
parseFloat('12.34'); // 12.34
parseInt('hello'); // NaN
parseFloat('hello'); // NaN
# ➕ Number
생성자 함수 vs parseInt
함수
- 숫자 타입으로 변환하는 방법 두 가지를 살펴보았다.
- 하지만
Number
생성자 함수와parseInt
함수에는 약간의 차이가 있다.
- 하지만
- 우선 두 방법 모두 문자열을 인자로 받으면 해당 문자열을 숫자로 바꿔주는 부분은 동일하다.
Number('1234'); // 1234
parseInt('1234'); // 1234
- 하지만 아래 예시처럼 문자열이 숫자 아닌 경우에는 출력되는 결과가 차이가 있다.
Number
생성자 함수는 파싱할 수 없는 문자가 하나라도 포함된 경우 무조건NaN
을 반환하고parseInt
함수는 문자열이 숫자로 시작하는 경우 숫자가 끝날 때까지만 파싱하여 형 변환을 한다.parseInt
함수도 시작이 숫자 형태가 아니라면Number
함수와 마찬가지로NaN
을 반환한다.
Number('1000km'); // NaN
parseInt('1000km'); // 1000
Number('거리:1000km'); // NaN
parseInt('거리:1000km'); // NaN
Number('1,000'); // NaN
parseInt('1,000'); // 1
# 📍 *
산술 연산자를 이용하는 방법
// 문자열 타입 => 숫자 타입
'0' * 1; // 0
'-123' * 1; // -123
'12.34' * 1; // 12.34
'12.00' * 1; // 12
// 불리언 타입 => 숫자 타입
true * 1; // 1
false * 0; // 0
// null, undefined 타입
null * 1; // 0
undefined * 1; // NaN
// 참조 타입
[] * 1; // 0
[2] * 1; // 2
['34'] * 1'; // 34
['3'] * ['2']; // 6
# (3) 불리언 타입으로 변환
# 📍 Boolean
생성자 함수를 new
연산자 없이 호출하는 방법
// 문자열 타입 => 불리언 타입
Boolean('*'); // true
Boolean(''); // false
Boolean('false'); // true
// 숫자 타입 => 불리언 타입
Boolean(0); // false
Boolean(1); // true
Boolean(-1); // true
Boolean(NaN); // false
Boolean(Infinity); // true
// null, undefined 타입 => 불리언 타입
Boolean(null); // false
Boolean(undefined); // false
// 참조 타입 => 불리언 타입
Boolean({}); // true
Boolean([]); // true
# 4. 타입 변환 내부 연산 자세히 살펴보기
이번 절은 책에 나와 있지 않은 내용이지만 타입 변환과 관련하여 더 공부하면서 새롭게 알게 된 내용들을 추가했다.
그 중에서도 자바스크립트에서 숫자가 아닌 값에서 숫자로 강제 변환할 때 내부적으로 어떤 일이 일어나는지 살펴보도록 하자.
# (1) 숫자로 타입 변환시 내부적으로 사용되는 주요 연산
- 숫자로 강제 변환할 때 내부적으로 사용되는 연산 네 가지를 먼저 살펴보도록 하자.
- 참고로 자바스크립트의 내부 로직은 원래 C++ 언어로 구성되어 있다.
- 하지만 이 로직을 자바스크립트 언어로 보기 쉽게 구현해놓은 코드가 있어 자바스크립트 버전으로 올려놓았다.
# 📍 Typeof(value)
- 함수의 인자 값이 어떤 type인지 반환하는 함수이다.
function TypeOf(value) {
const result = typeof value;
switch (result) {
case 'function':
return 'object';
case 'object':
if (value === null) {
return 'null';
} else {
return 'object';
}
default:
return result;
}
}
# 📍 ToNumber(argument)
- 함수의 인자 값을 숫자로 변환하는 내부 로직이다.
function ToNumber(argument) {
if (argument === undefined) {
return NaN;
} else if (argument === null) {
return +0;
} else if (argument === true) {
return 1;
} else if (argument === false) {
return +0;
} else if (TypeOf(argument) === 'number') {
return argument;
} else if (TypeOf(argument) === 'string') {
return parseTheString(argument); // not shown here(내부 로직이 상당히 복잡하다고 함...)
} else if (TypeOf(argument) === 'symbol') {
throw new TypeError();
} else if (TypeOf(argument) === 'bigint') {
throw new TypeError();
} else {
// argument is an object
const primValue = ToPrimitive(argument, 'number');
return ToNumber(primValue);
}
}
# 📍 ToPrimitive(input, hint)
- 어떤 형태든지 원시값(
number
,string
,boolean
,null
,undefined
중 하나)으로 변환하는 로직이다. - 이 때 두 번째 인자인
hint
에 의해 어떤 타입으로 변환할지 구분 기준을 세울 수 있다.'string'
,'number'
,'default'
세 가지 중 하나가 올 수 있는데'string'
,'number'
는 말 그대로 문자열, 숫자 타입으로 변환하겠다는 의미이다.'default'
는 연산자가 기대하는 자료형이 확실하지 않을 때를 의미하며 아주 드물게 발생한다.
function ToPrimitive(input: any, hint: 'string' | 'number' | 'default' = 'default') {
if (TypeOf(input) === 'object') {
const exoticToPrim = input[Symbol.toPrimitive];
if (exoticToPrim !== undefined) {
const result = exoticToPrim.call(input, hint);
if (TypeOf(result) !== 'object') {
return result;
}
throw new TypeError();
}
if (hint === 'default') {
hint = 'number';
}
return OrdinaryToPrimitive(input, hint);
}
// input is already primitive
return input;
}
- 내부 로직 설명
케이스 | 설명 |
---|---|
input 의 타입이 object 이고 Symbol.toPrimitive 메소드가 있는 경우 | 결과가 object 이면 TypeError 를 발생시키고 아니면 그 결과를 반환한다. |
input 의 타입이 object 이고 Symbol.toPrimitive 메소드가 없는 경우 | 다음에서 설명할 OrdinaryToPrimitive 함수를 실행하고 이 때 hint 값은 별도의 세팅이 없는 경우 'number' 로 설정된다. |
input 타입이 object 가 아닌 경우 | input 을 반환한다. |
Symbol.toPrimitive
- 목표로 하는 타입(hint)을 명명하는 데 사용된다.
obj[Symbol.toPrimitive] = function(hint) { // 반드시 원시값을 반환해야 한다. // hint는 "string", "number", "default" 중 하나가 될 수 있다. };
- 실제 예시 코드를 보면서 어떤 느낌이지 파악해보자.
const user = { name: "John", money: 1000, [Symbol.toPrimitive](hint) { alert(`hint: ${hint}`); 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
- 위 코드와 같이
Symbol.toPrimitive
내장 심볼을 이용하여 모든 종류의 형 변환을 다룰 수 있다.
# 📍 OrdinaryToPrimitive(O, hint)
- O에 내장된
toString
또는valueOf
메서드를 통해 타입이Object
인 O을 원시값으로 변환한다. toString
,valueOf
메서드를 통해 원시값으로 변환이 되지 않으면TypeError
를 발생한다.
// 특정 메서드를 호출 가능한지 판별하는 함수
function IsCallable(x) {
return typeof x === 'function';
}
function OrdinaryToPrimitive(O: object, hint: 'string' | 'number') {
let methodNames;
if (hint === 'string') {
methodNames = ['toString', 'valueOf'];
} else {
methodNames = ['valueOf', 'toString'];
}
for (const name of methodNames) {
const method = O[name];
if (IsCallable(method)) {
const result = method.call(O);
if (TypeOf(result) !== 'object') {
return result;
}
}
}
throw new TypeError();
}
- 내부 로직 설명(
hint
가'string'
인 경우)- 참고로
hint
가'number'
인 경우는 아래와 동일하고valueOf
=>toString
순서로 메서드를 확인하면 된다.
- 참고로
케이스 | 설명 |
---|---|
O.toString 을 호출할 수 있고 O.toString.call(O) 의 반환 타입이 'object' 가 아닌 경우(즉, 원시값인 경우) | 그 결과를 즉시 반환한다. |
O.toString 을 호출할 수 있고 O.toString.call(O) 의 반환 타입이 'object' 인 경우 | O.valueOf 를 호출할 수 있는지 확인 후 O.valueOf.call(O) 의 반환 타입이 'object' 일 때만 그 결과를 반환한다. |
O.valueOf 를 호출할 수 없고 O.valueOf.call(O) 의 반환 타입이 'object' 인 경우 | 원시값이 아니므로 TypeError 를 반환한다. |
toString
,valueOf
메서드
메서드 | 설명 |
---|---|
toString | 문자열로 변환해준다. |
valueOf | 객체 자신을 반환한다. |
// 객체인 경우
const user = {name: "John"};
alert(user); // [object Object]
alert(user.valueOf() === user); // true
- 참고로
ToPrimitive
함수에서Symbol.toPrimitive
를 직접 만든 예시 코드가 있었는데 이를 아래와 같이toString
,valueOf
메서드를 조합해서 동일하게 동작하도록 만들 수도 있다.
const 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
# (2) 숫자로 타입 변환 케이스 내부 연산 따라가기
# ✔️ Number('1');
Number
생성자 함수는ToNumber(argument)
연산이 작동된다.'1'
의 type이'string'
이므로parseTheString
내부 연산을 거쳐서 숫자1
이 된다.- 참고로
parseTheString
프로세스는 다소 복잡하여 ECMAScript 공식 문서 (opens new window)로 대체한다.
- 참고로
# ✔️ Number([1]);
ToNumber(argument)
연산argument
의 타입이object
이므로ToPrimitive(input, hint)
연산으로 넘어간다.- 참고로 마지막에
ToNumber(argument)
연산 남은 부분이 있어 다시 돌아올 예정이다.
ToPrimitive(input, hint)
연산input
값은[1]
,hint
는 주어지지 않으면'default'
가 된다.[1]
의 타입이'object'
이고[1][Symbol.toPrimitive]
는undefined
이므로OrdinaryToPrimitive(O, hint)
연산으로 넘어간다.- 이 때 다음 연산으로 넘어갈 때
hint
는'number'
가 된다.
OrdinaryToPrimitive(O, hint)
연산- 현재 호출되는 연산은
OrdinaryToPrimitive([1], 'number')
이 된다. hint
가'number'
이므로valueOf
=>toString
순서로 해당 메서드를 호출할 수 있는지 확인할 것이다.valueOf
메서드의 결과는[1]
이고 이 때 타입은'object'
이므로 다음 메서드로 넘어간다.toString
메서드의 연산 결과는Array.prototype.toString()
에 의해'string'
타입인'1'
이 되고'object'
타입이 아니므로'1'
을 반환한다.
- 현재 호출되는 연산은
ToNumber(argument)
연산- 이제 마지막으로
ToNumber('1')
연산을 실행한다. - 이 과정은 처음에 살펴보면
Number('1');
과 동일하므로 최종 결과는 숫자1
이 된다.
- 이제 마지막으로
# ✔️ Number([1, 2]);
Number([1]);
예제와 거의 동일하여OrdinaryToPrimitive(O, hint)
연산부터 살펴보도록하자.
OrdinaryToPrimitive(O, hint)
연산- 현재 호출되는 연산은
OrdinaryToPrimitive([1, 2], 'number')
이 된다. hint
가'number'
이므로valueOf
=>toString
순서로 해당 메서드를 호출할 수 있는지 확인할 것이다.valueOf
메서드의 결과는[1, 2]
이고 이 때 타입은'object'
이므로 다음 메서드로 넘어간다.toString
메서드의 연산 결과는Array.prototype.toString()
에 의해'string'
타입인'1,2'
이 되고'object'
타입이 아니므로'1,2'
을 반환한다.
- 현재 호출되는 연산은
ToNumber(argument)
연산- 이제 마지막으로
ToNumber('1,2')
연산을 실행한다. '1,2'
는 숫자로 변환 가능한 문자열이 아니므로 최종 결과는NaN
이 된다.
- 이제 마지막으로
- Chapter7에서 아래와 같은 예시 코드를 본 적이 있을 것이다.
// 아래 코드는 과연 어떤 결과를 반환할까? 한번 생각해보자.
var v5 = ['34'];
console.log(+v5); // ?
var v6 = ['34', '5'];
console.log(+v6); // ?
var v7 = undefined;
console.log(+v7); // ?
var v8 = null;
console.log(+v8); // ?
- 그 때는 어떤 결과가 나올지 생각만 해보았는데 이제는 타입 변환에 대해서 자세히 살펴보았기 때문에 어떤 결과가 나올지 예측할 수 있다.
var v5 = ['34'];
console.log(+v5); // 34
var v6 = ['34', '5'];
console.log(+v6); // NaN
var v7 = undefined;
console.log(+v7); // NaN
var v8 = null;
console.log(+v8); // 0
- 그렇다면 아래 코드는 어떤 결과가 나올까?
var v9 = [null];
console.log(+v9); // ?
var v10 = [undefined];
console.log(+v10); // ?
- 정답은 둘다 숫자
0
이 된다.- 왜냐하면
Array.prototype.toString()
메소드가join
메소드와 동일한 동작을 하는데 이 때join
메소드를 수행할 때 배열의 원소가null
,undefined
인 경우 빈 문자열로 대체된다고 한다. - 그렇기 때문에
[null].toString()
,[undefined].toString()
모두 빈 문자열''
이 되고 최종 결과는+'' = 0
이 되는 것이다. - 자세한 내용은 Array.prototype.toString() - MDN 공식 문서 (opens new window)와 Array.prototype.join() - MDN 공식 문서 (opens new window)를 참고하자.
- 왜냐하면
- 지금까지 살펴본 내용을 토대로
+v11
와+v12
가 어떤 값으로 평가될지 예측해보자.
var v11 = [null, undefined];
console.log(+v11); // ?
var v12 = ++[[]][+[]]+[+[]];
console.log(+v12); // ?
🔖 함께 보면 좋은 자료(feat. Reference)
- 지금은 숫자로 강제 타입 변환할 때만 살펴보았지만
string
,boolean
등 다양한 타입으로 변환하는 경우도 있는데 이는 여기 (opens new window)(Javascript 언어로 작성한 타입 변환 내부 연산)를 참고하면 자세히 알 수 있다. - 그리고 모던 Javascript 튜토리얼에서도 객체를 원시형으로 변환하는 내용을 여기 (opens new window)에 자세히 나와있으니 함께 참고하면 좋을 것 같다.
# 5. 단축 평가(short-circuit evaluation)
# (1) 논리 연산자를 사용한 단축 평가
- 논리 연산자 중에서 논리곱 연산자(
&&
)와 논리합 연산자(||
)는 논리 연산의 결과를 결정하는 피연산자를 타입 변환하지 않고 그대로 반환하고 이를 단축 평가라고 한다. - 단축 평가는 표현식을 평가하는 도중에 평가 결과가 확정된 경우 나머지 평가 과정을 생략하는 것을 말한다.
단축 평가 표현식 | 평가 결과 |
---|---|
true || anything | true |
false || anything | anything |
true && anything | anything |
false && anything | false |
'apple' || 'banana'; // 'apple'
true || 'apple'; // true
'apple' || true; // 'apple'
false || 'apple'; // 'apple'
'apple' || false; // 'apple'
'apple' && 'banana'; // 'banana'
true && 'apple'; // 'apple'
'apple' && true; // true
false && 'apple'; // false
'apple' && false; // false
- 단축 평가를 사용하면
if
문을 대체할 수 있다.- 어떤 조건이 Truthy 값일 때 무언가를 해야 한다면 논리곱 연산자 표현식으로
if
문을 대체할 수 있다. - 반면 어떤 조건이 Falsy 값일 때 무언가를 해야 한다면 논리합 연산자 표현식으로
if
문을 대체할 수 있다.
- 어떤 조건이 Truthy 값일 때 무언가를 해야 한다면 논리곱 연산자 표현식으로
// before
const done = true;
let message = '';
if (done) {
message = '완료';
}
// after
console.log(done && '완료'); // '완료'
// before
const done = false;
let message = '';
if (done) {
message = '미완료';
}
// after
console.log(done || '미완료'); // '미완료'
# (2) 객체와 함수에서 단축 평가 적용 예시
# 📍 객체를 가리키기를 기대하는 변수가 null
또는 undefined
가 아닌지 확인하고 프로퍼티를 참조할 때
// error 발생
const elem = null;
const value = elem.value; // TypeError: Cannot read property 'value' of null
// 단축 평가로 에러 해결
const elem = null;
const value = elem && elem.value; // null
# 📍 함수 매개변수에 기본값을 설정할 때
// 단축 평가를 사용한 매개변수의 기본값 설정
function getStringLength(str) {
return (str || '').length;
}
getStringLength(); // 0
getStringLength('hello'); // 5
// ES6의 매개변수의 기본값 설정
function getStringLength(str = '') {
return str.length;
}
getStringLength(); // 0
getStringLength('hello'); // 5
# ➕ ESLint - no-param-reassign
- 참고로 책에 나와 있는 예시 코드는 위 이미지와 같다.
- 하지만 이렇게 작성하는 경우 함수 매개 변수로 들어온 값을 임의로 변경하는 동작을 수행하는 것이다.
- 비록 지금은 값이 없는 경우 빈 문자열로 처리하는 것 밖에 없지만 다른 코드에서 함수 매개 변수를 받아서 수정 또는 재할당하는 동작을 수행하게 되면 의도하지 않은 동작을 할 수 있기 때문에 이러한 코드 패턴은 권장하지 않는다.
- 그래서 이러한 패턴을 피해서
(str || '').length
와 같이 인라인화하여 코드를 수정했다. - 또는 두 번째
getStringLength
함수와 같이 매개 변수의 기본값을 지정하는 방법으로 작성해도 좋다. - ESLint의
no-param-reassign
옵션에 대한 상세 내용은 여기 (opens new window)를 참고해보자.
# (3) 옵셔널 체이닝 연산자(optional chaining operator)
- ES2020에서 도입된 옵셔널 체이닝 연산자(
?.
)는 좌항의 피연산자가null
또는undefined
인 경우undefined
를 반환하고, 그렇지 않으면 우항의 프로퍼티 참조를 이어간다.
const elem = null;
const value = elem?.value; // undefined
- 논리곱 연산자(
&&
)는 좌항 피연산자가false
로 평가되는 Falsy 값이면 좌항 피연산자를 그대로 반환한다.- 좌항 피연산자가 Falsy 값인
0
이나 빈 문자열인 경우도 마찬가지다.
- 좌항 피연산자가 Falsy 값인
- 반면 옵셔널 체이닝 연산자는 좌항 피연산자가
false
로 평가되는 Falsy 값이라도null
또는undefined
가 아니면 우항의 프로퍼티 참조를 이어간다.
const str = '';
console.log(str && str.length); // ''
console.log(str?.length); // 0
# (4) null 병합 연산자(nullish coalescing operator)
- ES2020에서 도입된 null 병합 연산자(
??
)는 좌항의 피연산자가null
또는undefined
인 경우 우항의 피연산자를 반환하고, 그렇지 않으면 좌항의 피연산자를 반환한다. - null 병합 연산자는 변수에 기본값을 설정할 때 유용하다.
const foo = null ?? 'default';
console.log(foo); // 'default';
- 논리합 연산자(
||
)는 좌항 피연산자가false
로 평가되는 Falsy 값이면 우항의 피연산자를 반환한다.- 만약 Falsy 값인
0
이나 빈 문자열도 기본값으로 유효하다면 예기치 않은 동작이 발생할 수 있다.
- 만약 Falsy 값인
const foo = '' || 'default';
console.log(foo); // 'default'
- 반면 null 병합 연산자는 좌항 피연산자가
false
로 평가되는 Falsy 값이라도null
또는undefined
가 아니면 좌항의 피연산자를 그대로 반환한다.
const foo = '' ?? 'default';
console.log(foo); // ''