# 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 // 0null,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); // ''