javascript 의 말 많은 강제변환에 대해 정리해보자!
명시적 / 암시적 강제변환
책에서는 자바스크립트의 강제 변환 타입을 다음 두 가지로 정의하고 있다.
명시적 변환과 암시적 변환은 작동하는 방식에서도 조금 차이가 있다. (책에서는 미묘하다고 표현한다.)
암시적 강제변환
다른 작업 도중 불분명한 부수 효과로 변환 된다.
아래 예시에서 b 는 공백 문자열 "" 과의 + 연산을 처리하면서 9 를 동등한 문자열인 "9" 로 강제 변환한다.
이는 + 연산을 하면서 발생시킨 부수효과로 암시적 강제변환이다.
const a = 9;
const b = a + "" // "9"
명시적 강제 변환
의도적으로 타입변환을 발생시킨다.
c 는 명백하게 String() 함수를 이용해 문자열 타입으로 변환하고 있다. 이는 명시적 강제변환이다.
const c = String(a)
추상연산
추상연산은 암시적이던 명시적이던 타입 변환이 일어날 때, 내부적으로 수행되는 연산이다. 한 개 이상의 연산이 수행될 수 있다.
값이 어떻게 변환되는지 기본 규칙을 알아보기 위해 ES5 에 정의된 추상 연산에 정의된 ToString, ToNumber, ToBoolean, ToPrimitive 를 살펴보자.
ToString
문자열이 아닌 값 -> 문자열 변환
작업을 담당한다.
- 내장 원시 값은 본연의 문자열화 방법이 정해져 있다.
- null -> "null", undefined -> "undefined", true -> "true"
- 숫자는 그냥 문자열로 바뀐다. 너무 작거나 큰 값은 지수 형태로 바뀐다.
- 일반 객체는 기본적으로
Object.prototype.toString()
에 있는 toString() 메서드가 내부[[Class]]
를 반환한다.
이미 toString() 함수가 있는 객체는 본인 걸 호출한다. 사실 객체 => 문자열 강제 변환시 ToPrimitive 과정을 거치지만 이건 뒤에서 다룬다. - 배열은 기본적으로 재정의된 toString() 이 있다.JSON 문자열화
JSON.stringify()
는 인자가 undefined, 함수, 심벌 같은 안전 값이 아니면 자동으로 누락시킨다. 환형 참조 객체를 넘기면 에러가 발생한다. 만약 이런 값이 배열에 포함되어 있으면 null 로 바꾼다. - 직렬화할 수 없는 객체값을 문자열화 하려면
toJSON()
메서드를 따로 정의하면 된다.toJSON()
으로 string 화 할 수 있는 값을 반환하게 하고JOSN.sringify()
가 문자열화 처리를 할 수 있게 해주면 된다. JSON.stringify()
는 어떤 값을 JSON 문자열로 직렬화하는 함수다.
이는 강제변환과 똑같지는 않지만, ToString 규칙과 관련이 있다. JSON 문자열화나 toString() 변환이나 기본적으로 같은 로직이다.const a = [1,2,3]; a.toString(); // "1,2,3"
JSON.stringify() 의 두번째 인자에 배열 혹은 함수형태의 대체자를 넣어줄 수 도 있다.
해당 배열과 함수는 문자열화 할 프로퍼티를 필터링하는 역할을 한다. 함수의 경우 객체를 재귀적으로 돌면서 직렬화 해준다.
대체자가 배열일 경우, 배열의 전체 원소는 문자열이여야 한다. 또한 각 원소는 직렬화할 대상의 프로퍼티를 적어준다.
대체자가 함수면 처음 한번은 객체 자신, 그 다음에는 각 객체의 프로퍼티 별로 한 번씩 실행한다. 직렬화에서 제외하고 싶은 프로퍼티는 undefined 를 반환하면 된다.
const a = {
b : 10,
c : "10",
d : [1,2,3]
}
JSON.stringify(a, ["b", "c"]); // "{"b":10, "c":10"}" b, c 만 직렬화 되었다.
JSON.stringify(a, function(k,v) {
if (k !== "c") return v; // c poperty 는 제외된다.
});
// "{"b": 10, "d": [1,2,3]}"
세 번째 인자로는 space
로 들여쓰기 간격을 정할 수가 있다.
JSON.stringify(a, null, 3);
//하면 아래처럼 출력된다.
"{
"b": 10,
"c": "10",
"d": [
1,
2,
3
]
}"
ToNumber
숫자가 아닌 값 -> 수식 연산이 가능한 숫자로 변환한다.
- true -> 1 / false -> 0
- undefined -> NaN
- null -> 0
- 실제로 Number(null) 하면 0 이 나온다.
ToBoolean
자바스크립트에서 강제변환하면 false 가 나오는 값은 아래와 같아. (명세에서 정의한 falsy 한 값이다.)
- undefined
- null
- false
- +0, -0, NaN
- ""
이 외에는 모두 true 로 간주한다. 'falsy' 값 목록에 없으면 'truthy' 한 것으로 간주한다.
명시적 강제 변환
문자열 ↔ 숫자
String() 과 Number()
앞에 new 키워드가 붙지 않기 때문에 객체 래퍼를 생성하진 않는다. (단순 함수 호출과 동일하다는 것인가....)
toString()
문자열로 변환한다는 의미가 뚜렷하므로 명시적인 것처럼 보이지만, 암시적인 요소가 숨어있다.
보통 숫자의 원시값에는 toString() 메서드가 없기 때문에 엔진에서 자동으로 객체 레퍼로 '박싱' 한다.
+ 단항 연산자
이것도 명시적이라고 할 수 있을까? + 가 숫자로 강제변환하는 기능이 있기 한다.
책에서는 관점에 따라 다르다는 식으로 설명하고 명확히 결론짓지는 않는 듯 하다.
저자는 가급적 +/- 단항 연산자를 다른 연산자와 인접하게 사용하는 것은 권장하고 있지 않다. 예를 들어
1 + - + + - + 1
이런 식....(누가 이렇게 쓰지)
var a = 11;
var b = String(a); // "11"
var c = Number("12.5") // 12.5
// toString()
var k = 21;
var k_s = k.toString(); // "21"
// use + operation
var j = "3.14"
var j_number = +j // 3.14
날짜 → 숫자
+ 단항 연산자 는 Date 객체 → 숫자로 강제 변환하는 용도로도 쓰인다.
하지만 getTime() 을 호출하거나 정적함수인 now() 를 호출하는 방법도 있다.
저자는 now() 호출하는 방식을 권장한다. 날짜 타입에 관한 한 강제변환은 권하지 않는다. 아마 날짜의 경우 명시적인 것이 가독성이 더 좋아서..? 인가 싶다.
var date = new Date(); // Sun Jan 21 2024 11:11:11 GMT+0900
+date; // 1705803074832
var timestamp = new Date().getTime() // 1705803074832
// ES5
var timestamp2 = Date.now() // 1705803074832
숫자 형태의 문자열 파싱
문자열 → 숫자 강제변환과 비슷하지만, 차이가 있다.
문자열로부터 숫자 값을 파싱 은 숫자가 아닌 문자형(Non-Number Character) 을 허용한다.
좌 → 우 방향으로 파싱하다가 숫자가 아닌 문자를 만나면 멈춘다.
var str = "21";
var str2 = "21vh";
Number(str); // 21
parseInt(str); // 21
Number(str2); // NaN
parseInt(str2); // 21
책은 파싱은 강제변환의 대안이 될 수 없다고 한다. 숫자가 아닌 문자열도 변환이 가능하기 때문에 그런 듯 하다.
pasrseInt 에는 비문자열인 값을 넘기는 것은 좋지 않다. 강제로 문자열로 변환하는 과정이 추가되기 때문에 이는 지양해야 한다.
참고로 ES5 이전에는 parseInt 에서 두번째 인자로 진법을 넣어주지 않으면, 첫번째 문자열을 보고 몇진수의 숫자로 바꿀지 결정했다. (그래서 eslint 규칙중에도 두번째 인자를 넣어주지 않으면, 오류를 뱉는 규칙도 있다.)
그 뒤에 parseInt 의 비문자열 파싱절에서 자바스크립트를 대변하는(?) 저자의 구절이 재밌다. 관심있으면 찾아서 읽어보자.
비 불리언 → 불리언
Boolean(a) 이나 !!a 같은 명시적 강제변환을 사용하자. (삼항연산자 같은 거말고..)
암시적 강제변환
책에서는 부수 효과가 명확하지 않게 숨겨진 형태로 일어나는 타입변환이라고 정의하고 있다.
(보기에 분명하지 않은 타입변환은 모두 이 범주에 속한다고도 말한다.)
자바크립트는 암시적 강제변환 때문에 사람들의 노여움(?)을 사기도 했던 모양인데, 저자는 이에 대해 암시적 강제 변환의 중요한 목적은 불필요한 상세구현을 줄이는 것이라고 말해준다.
(여러 종류의 강제변환에 대해서 책은 설명해주는데, 너무 많아 실제로 경험했던 부분들만 정리한다.)
비불리언 → 불리언
아래와 같은 상황에서 불리언으로 암시적 강제변환이 일어난다.
- if() 문의 조건 표현식
- for ( ; ; ) 에서 두번재 조건 표현식
- while() 루프의 조건 표현식
- ? 삼항 연산자의 조건 표현식
- || 및 && 연산자의 좌측 피연산자
&& 와 || 연산자
자바스크립트는 다른 언어와 달리 &&
와 ||
연산의 결과 값이 논리 값(불리언)이 아니다!
결과 값은 두 피연산 자 중 한쪽 값이다. 즉, 두 피연산자의 값들 중 하나를 선택한다.
||
: 결과 값이 true 면 첫번째 피연산자를, false 면 두 번째 피연산자 값을 반환한다.- '&&' : true 면 두 번째 피연산자의 값을, false 면 첫 번째 피연산자의 값을 반환한다.(첫번째 표현식이 두번째 표현식의 가드 역할을 한다.)
아래 예시를 보면 더 명확하게 이해가 될 것이다.
var a = 42;
var b = "abc";
var c = null;
a || b // 42
a && b // "abc"
c && b // null
느슨한/엄격한 동등 비교
느슨한 동등 비교
== 을 사용한다. 강제변환을 허용한다.
타입이 동일하지 않을 경우, 강제변환 후 값을 비교한다.
엄격한 동등 비교
=== 을 사용한다. 값만 비교 하기 때문에 강제 변환이 일어나지 않는다.
강제변환이 필요하다면 == 를, 필요 없다면 === 을 사용하자.
추상동등 비교
== 연산자 로직은 추상 동등 비교 알고리즘을 사용한다. (ES5 명세서 참고)
여기서 주의해야 할 것만 좀 나열해보자면
- NaN 은 그 사진과도 동등하지 않다.
- +0 과 -0 은 동등하지 않다.
- 객체의 동등 비교에서 강제변환은 일어나지 않는다. (즉, === 를 써도 똑같다.)
불리언과의 느슨한 동등 비교
불리언인 값을 ToNumber(x) 로 바꾸고 비교한다. 즉 truthy/falsy 를 비교하는 것이 아니다.
그러니까 == true, == false 같은 이상한 거 쓰지 말자.
null 과 undefined
== 비교시, null == undefined 면 true 를 반환한다.
책에서는 굳이 아래와 같이 쓰지 말자고 한다. 보기 흉하고 아주 사소하지만 성능도 떨어진다고...
if(a === undefined || a === null) {
}
그냥 if(a == null) 로만 해도 가독성 좋고 안전하게 작동하는 강제변환이 이루어진다.
강제변환에 대한 저자의 생각
자바스크립트는 비교구문에서 암시적 강제변환 때문에, 예상을 벗어나는 일이 있다. 그러나 저자는 그런 경우는 7가지 정도이며, 겨우 이것때문에 다른 좋은 강제변환 시스템을 쓰지 말아야할 이유는 없다고 한다.
아래만 잘 지키면 안전하게 잘 쓸 수 있다.
- 피연산자 중 하나가 true/false 일 가능성이 있으면 절대로 == 연산자를 쓰지 말자.
- 피연산자 중 하나가 [], " " , 0 이 될 가능성이 있으면 가급적 == 연산자는 쓰지 말자.
암시적 강제 변환이 나쁘다고 얘기하는 사람들이 많지만, 오히려 코드 가독성을 향상하는 장점도 있다! 고 말하고 있다.
'Javascript' 카테고리의 다른 글
[TypeScript] overload 함수에서 No overload matches this call 에러 (0) | 2023.01.26 |
---|---|
Typescript generic & util (0) | 2021.04.08 |
return function (0) | 2021.03.24 |
[You don't know JS] Chapter2 - 값 (0) | 2021.02.25 |
[You don't know JS] Chapter1 - 타입 (0) | 2021.02.23 |