타입변환은 코더의 의도에 따라 가시적으로 의도가 보이는 명시적 타입변환과 엔진에 의해 자동으로 변환되는 암묵적 타입변환이 있다. 단축평가는 값을 평가하여 코드 흐름을 제어할 떄 좀 더 간결하게 사용할 수 있는 연산문으로 대표되며 단축평가에서 예측가능한 코드를 위해 타입 변환이 어떻게 동작하는지 이해해보자.
자바스크립의 모든 값은 타입이 있고 이 타입은 개발자의 의도에 따라 다른 타입으로 변환할 수 있다.
이를 명시적 타입 변환 / explicit coercion, 또는 타입 캐스팅 type casting이라고 한다.
var x = 10;
//명시적 타입 변환
var str = x.toString(x);
console.log(typeof str, str); // 'string', '10'
console.log(x); // 10
// 그렇다고 기존 원시값과 타입이 변하지는 않는다.
반대로 자바스크립트가 표현식을 평가하는 도중에 엔진에 의해 개발자의 의도와 상관없이 암묵적으로 타입이 자동 변환되기도 하는데 이를 암묵적 타입 변환 / implicit coercion 또는 타입 강제 전환 / type coercion 이라고 한다.
var x = 10;
var str = x + '';
console.log(typeof str, str); // 'string', '10'
console.log(x); // 10
// 그렇다고 기존 원시값과 타입이 변하지는 않는다.
위 두 예제에서 봤듯이 명시적 타입 변환이든 암묵적 타입 변환이 일어나든 기존 원시 값을 직접 변경하는 것은 아니며 이미 정리했든 원시 값은 변경 불가능한 값, immutable value이다.
즉 기존 워시 값을 사용해 다른 타입의 새로운 원시 값을 사용하여 (생성하여) 평가에만 사용할 뿐 기존 변수에 재할당되지 않는다.
단지 엔진이 표현식을 에러 없이 평가하기 위해 타입 변환을 통해 새로운 값을 만들어 단 한 번 사용한 후 버린다.
명시적 타입 변환은 개발자의 의지와 의도가 코드에 명백히 드러나는 반면 암묵적 타입 강제 변환은 암묵적으로 자동 변환되기 떄문에 코드 흐름에 따라 그 의도와 의미 (값의 타입) 예측과 추적이 예측하기 힘들어질 수 있다. 이는 곧 오류 발생 확률을 높히기도 한다.
자바스크립트 엔진은 표현식을 평가할 때 코더의 의도와 상관없이 코드의 문맥을 고려해 암묵적으로 타입을 강제 변환할 때가 있다.
// 피연산자가 모두 문자열 타입이어야 하는 문맥
'10' + 2 // '102'
// 피연산자가 모두 숫자타입이어야 하는 문맥
5 * '10' // 50
// 피연산자 또는 표현식이 불리언 타입이어야 하는 문맥
!0 // true
if (1) {
// if문의 코드 블럭이 실행된다.
}
위 예제처럼 표현식을 평가할 때 문맥에 부합하지 않는 상황이 발생하면 자바스크립트는 가급적 에러를 발생시키지 않도록 암묵적 타입 변환을 통해 표현식을 평가한다.
암묵적 타입 변환이 발생하면 원시 타입
중 하나로 타입을 자동 변환한다.
1 + '2' // '12'
전에 정리했듯이 피연산자중 하나 이상이 문자열이면 +연산자
는 문자열 연결 연산자로 동작한다.
문자열 연결 연산자의 역할은 문자열 값을 만드는 것이므로 모든 피연산자는 코드의 문맥에서 모두 문자열 타입이어야 한다.
따라서 자바스크립트 엔진은 문자열 연결 연산자 표현식을 평가하기 위해 피연산자 중에서 문자열 타입이 아닌 피연산자를 문자열 타입으로 강제 변환한다.
이뿐만 아니라 템플릿 리터럴
은 표현식의 평가 결과를 문자열 타입으로 암묵적으로 변환하는데 이처럼 피연산자 뿐만 아니라 표현식 또한 코드 문맥에 부합이 필요하다면 자바스크립트 엔진은 암묵적 타입 변환을 실행한다.
`1 + 1 = ${1 + 1}` // "1 + 1 = 2"
문자열 타입이 아닌 값을 문자열 타입으로 강제 변환되는 예는 다음과 같다.
// 숫자타입
0 + '' // "0"
-0 + '' // "0"
1 + '' // "1"
-1 + '' // "-1"
NaN + '' // "NaN"
Infinity + '' // "Infinity"
-Infinity + '' // "-Infinity"
//boolean type
true + '' // 'true'
false + '' // 'false'
// null type
null + '' // 'null'
// undefined type
undefined + '' // 'undefined'
// Symbol type
(Symbol()) + '' // TypeError: Cannot convert a Symbol value to a string
// Object type
({}) + '' // '[object Object]'
Math + '' // '[object Math]'
[] + '' // ''
[10, 20] + '' // "10,20"
(function(){}) + '' // "function(){}"
Array + '' // "function Array() { [native code] }"
연산식에서 문자열 연결 연산자
을 제외한 모든 연산자는 산술 연산자
이며 그 역할은 숫자 값을 만드는 것이다. 즉, 모든 피연산자는 코드 문맥상 모두 숫자 타입이어야 한다.
따라서 산술 연산자 표현식을 평가하기 위해 산술 연산자의 피연산자 중에서 숫자 타입이 아닌 경우 숫자 타입으로 암묵적 타입 변환한다. 이때 숫자 타입으로 변환되지 않는 값은 NaN
이 된다.
또한 숫자 타입 변환이 일어나는 문맥은 산술 연산자뿐만 아니라 비교 연산자
에서도 일어난다.
비교 연산자에서도 숫자 타입으로 암묵적 타입 변환이 일어나는 이유는 크기를 비교해야하기 떄문이다.
앞서 연산자 부분에서 + 단항 연산자
는 피연산자가 숫자 타입의 값이 아닐 경우 숫자 타입으로 강제 변환한다고 했다.
+ 단항 연산자
를 통해 다른 타입들이 숫자타입으로 어떻게 변환되는지 알아보자.
+'' // 0
+'0' // 0
+'1' // 1
+'string' // NaN
+true // 1
+false // 0
+null // 0
+undefined // NaN
+Symbol() // TypeError: cannot convert a Symbol value to a number
+{} // NaN
+[] // 0
+[10,20] // NaN
+(function(){}) // NaN
runjs로 이것저것 하다가 재밌는것을 발견했다.
object 타입 피연산자에 숫자 피연산자를 + 연산자로 연산하는 표현식을 실행해보면 신기한 결과가 나온다.
object 타입의 문자열로의 타입 강제전환 후 string + number인 경우처럼 + 연산자가 문자열 연결 연산자로 작동한 결과처럼 보였다.
뇌피셜이긴 하지만 객체타입과 숫자타입의 피연산자를 + 연산자로 연산할 경우 객체타입은 문자열로 강제타입 변환 후 연산이 되는 것 같다.
// 분명 문자열 연결 연산자가 아닐것 이라고 생각했는데
// 마치 "" + 1 처럼 작용됐다.
[]+1 // "1"
// 분명 문자열 연결 연산자가 아닐것 이라고 생각했는데
// 마치 ({} + '') + 1 처럼 작용됐다.
{} + 1 // '[object Object]1'
반대로 - 연산자의 경우 숫자타입 변환 예제처럼 작동한다.
console.log([] - 1) // -1
console.log({} - 1) // NaN
불리언 타입으로 변환은 if 문이나 for문과 같은 제어문 또는 삼항 조건 연산자의 조선식을 활용할 때 사용된다.
if (true) console.log('1')
if ('') console.log('2')
if (0) console.log('3')
if ('str') console.log('4')
if (null) console.log('5')
// 1, 4
이때 자바스크립트 엔진은 불리언 타입이 아닌 값을 Truthy 값 또는 Falsy 값으로 구분한다.
즉 불리언 값으로 평가되어야 할 문맥 (ex: 제어문의 조건식)에서 Truthy 값은 true
로 Falsy 값은 false
로 불리언 타입으로 암묵적 타입 변환한다.
false
, undefined
, null
, 0
, -0
, NaN
, ""
Falsy 값들을 제외한 모든 값은 모두 true로 평가되는 Truthy 값이다.
//Truthy 값들
function isFalsy(value) {
return !value
}
function isTruthy(value) {
return !!value
}
isFalsy(false);
isFalsy(undefined);
isFalsy(null);
isFalsy(0);
isFalsy(-0);
isFalsy(NaN);
isFalsy('');
// all return false
isTruthy(true);
isTruthy('0');
isTruthy({});
isTruthy([]);
isTruthy(Infinity);
// all return true