본문 바로가기

JavaScript

[You don`t Know JS 정리] ch.4 강제변환

Ch4 강제변환

4.1 값 변환

  • 어떤 값을 다른 타입의 값으로 바꾸는 과정이 명시적이면 '타입 캐스팅', 암시적이면 '강제변환' 이라고 한다
  • 강제변환을 하면 문자열, 숫자, 불리언 중 하나가 된다. 박싱은 강제변환이 아니다.
  • 명시적 강제변환은 코드만 봐도 의도적으로 타입변환을 일으키는 것이 명백하고, 암시적 강제변환은 다른 작업 도중 발생하는 부수 효과이다.

4.2 추상 연산

4.2.1 ToString

  • 내장 원시 값은 본연의 문자열화 방법이 정해져 있다. 너무 크거나 작은 숫자값은 지수 형태로 바뀐다.
  • 일반 객체는 특별히 지정하지 않으면 기본적으로 toString() 메서드가 내부 [[Class]]를 반환한다 ([object object])
  • 배열은 모든 원소 값이 콤마(,)로 분리된 형태로 이어진다.
JSON 문자열화
  • 강제변환과 똑같지는 않지만, ToString 규칙과 관련이 있다.
  • JSON 안전 값은 모둔 JSON.stringify()로 문자열화 할 수 있다.
  • JSON 안전 값이 아닌 것들은 undefined, 함수, 심벌, 환형 참조 객체이다. 이들은 표준 JSON 객체 형식을 벗어난 값이다.
  • JSON.stringify()는 인자가 안전 값이 아닌 것들은 자동으로 누락시키며 만약 배열에 포함되어 있으면 null로 바꾼다. 객체 프로퍼티에 있다면 지워버린다.
  • toJSON()의 역할은 '문자열화하기 적당한 JSON 안전 값으로 바꾸는 것'이지 JSON 문자열로 바꾸는 것이 아니다.
  • JSON.stringify()의 두번째 인자로 배열 또는 함수가 들어가는 데, 객체를 필터링 하는 방법으로 쓰인다.
  • JSON.stringify()의 세번째 인자는 스페이스라고 하며, 들여쓰기를 할 수 있다. 빈 공간의 갯수를 숫자로 지정하거나 문자열을 지정하여 들여쓰기에 사용한다.

4.2.2 ToNumber

  • 숫자 아닌 값 -> 수식 연산이 가능한 숫자 변환 로직은 ES5 9.3 ToNumber 추상 연산에 나와있다. (https://www.ecma-international.org/ecma-262/5.1/)
  • 예를 들어 true는 1, false는 0, undefined는 NaN, null은 0으로 바뀐다.
  • 변환이 실패하면 에러를 내지 않고 NaN을 반환한다.
  • 객체 변환의 경우 일단 동등한 원시 값으로 변환 후 그 결괏값을 ToNumber 규칙에 의해 강제변환한다.
  • 객체의 경우 일단 valueOf()를 쓸 수 있고 반환 값이 원시 값이면 그대로 강제변환한다.
  • 그렇지 않을 경우 toString() 메서드를 이용하여 강제변환한다.
  • 어찌해도 원시 값으로 바꿀 수 없느 경우는 TypeError 오류를 던진다.

4.2.3 ToBoolean

Falsy 값
  • 자바스크립트의 모든 값은 불리언으로 강제변환하면 falsy 값이 되거나 truthy 값이 되거나 둘 중 하나다.
  • 명세가 정의한 falsy 값은 다음과 같다
undefined
null
false + 0, -0, NaN
;("")
  • 이 목록에 있으면 falsy 값이며 불리언으로 강제변환 시 false가 된다.
  • 위 목록에 없는 나머지는 truthy 값이다.
truthy 값
  • falsy 목록에 없으면 무조건 truthy 값이다.

'false'
'0'
"''"
[]
{}
function() {}
  • 위 내용은 모두 truthy 값이다.
  • 그렇기 때문에 얼마 안되는 falsy 값만 알고 있으면 된다.

4.3 명시적 강제변환

  • 분명하고 확실한 타입변환이다.

4.3.1 명시적 강제변환: 문자열 <-> 숫자

  • String()과 Number() 함수를 이용한다. 앞에 new 키워드는 붙지 않는다.
var a = 42
var b = String(a) // "42"

var c = "3.14"
var d = Number(c) // 3.14
  • 아래 예 처럼 단항 연산자 +를 사용해서 피연산자 c를 숫자로 명시적 강제변환 할 수 있다.
var c = "3.14"
var d = +c // 3.14
  • 지그재그 과제 전형에서 이렇게 한 번 쓴 적이 있는데 명시적으로 변환하는게 좋겠다는 피드백을 받았다.
  • 개발자의 경험과 시야에 따라 다르다고 한다.
  • 가급적 +/- 단한 연산자를 다른 연산자와 인접하여 사용하지 말자. 혼동을 줄이는 게 좋다.
날짜 -> 숫자
  • 단항 연산자 +는 'Date 객체 -> 숫자' 강제변환 용도로도 쓰인다. 날짜 시각값을 유닉스 타임스탬프 표현형으로 나타낸다.
  • 강제변환을 하지 않고 Date 객체로부터 타임스탬프를 얻는 방법은 new Date().getTime()이다.
  • 현재 타임 스탬프는 Date.now()로 얻을 수 있다.
  • 날짜 타입에 관한 한 강제변환은 권하고 싶지 않다고 한다.
이상한 나라의 틸드(~)
  • 비트 연산을 하면 피연산자는 32비트 값으로 강제로 맞춰진다. 이 역할은 ToInt32가 한다.
  • ToInt32는 ToNumber 강제변환을 한다.
  • ~ 연산자는 먼저 32비트 숫자로 강제변환 후 NOT 연산을 한다. (각 비트를 거꾸로 뒤집음)
  • ~ 연산자가 쓸모 있는 경우는 indexOf() 같은 함수에 >= 0 이나 !== -1 과 같은 코드를 쓰지 않을 수 있기 때문이다.
var a = "Hello World"

~a.indexOf("lo") // -4. 3의 2의 보수인 -4를 구한다.

if (~a.indexOf("lo")) {
  // 어쩌구
}
비트 잘라내기
  • ~~는 32비트 값에 한하여 Math.floor() 와 같은 결과를 내지만 음수일 경우에는 다르다.
  • 여러분의 코드를 읽을 주변 동료 개발자가 연산자의 작동 원리를 적절히 이해하고 있다는 전제하에!!! 틸드를 잘 활용하기를 바란다.

4.3.2 명시적 강제변환: 숫자 형태의 문자열 파싱

  • 문자열로부터 숫자 값의 파싱은 비숫자형 문자를 허용한다. 좌 -> 우 방향으로 파싱하다가 숫자 같지 않은 문자를 만나면 즉시 멈춘다.
  • 파싱은 강제변환이 아니다. 목적이 다르다.
  • parseInt()는 문자열에 쓰는 함수이다. 절대로 parseInt()에 비문자열 값을 넘기지 말자
  • ES5 이후에는 두번째 인자가 없어도 알아서 10진수로 처리를 한다.
  • 이상하게 사용해서 말도 안 되는 결과가 나왔다고 자바스크립트를 탓하지 말자.

4.3.3 명시적 강제변환: * -> 불리언

  • Boolean() 함수는 명시적인 강제 변환 방법이다.
  • ! 단항 연산자도 값을 불리언으로 명시적으로 강제 변환한다.
  • 명시적인 강제변환을 할 땐 !! 이중부정 연산자를 사용한다.
  • if() 문 등읠 불리언 콘텍스트에서도 암시적인 강제 변환이 일어난다.
  • 삼항 연산자에서도 표현식의 평과 결과에 따라서 명시적으로 암시적이라고 할 수 있는 변환이 일어난다. 무조건 쓰지 말자고 하는 데 굉장히 유용하고 많이 쓰이는 코드 중 하나이다.

4.4 암시적 변환

  • 부수 효과가 명확하지 않게 숨겨진 형태로 일어나는 타입 변환
  • 암시적 강제변환의 목적은 불필요한 상세 구현을 줄이는 것

4.4.1 암시적이란?

  • 예를 들어 a 타입을 b 타입으로 변경하기 위해서 중간 단계로 c를 거치는 경우가 있다.
  • TypeB(TypeC(a)) -> TypeB(a) 이렇게 코드를 쓸 수 있다면 타입변환을 단순화 했다고 할 수 있다.
  • 저자는 실제로 이것이 코드 가독성을 높이고 세세한 구현부를 추상화하거나 감추는 데 도움이 된다고 생각한다.
  • 암시적 강제변환은 우리가 작성하는 코딩에 도움을 줄 수 있다.

4.4.2 암시적 강제변환: 문자열 <-> 숫자

    • 연산자는 '숫자의 연산', '문자열 접합' 두가지 기능이 있다.
var a = "42"
var b = "0"

var c = 42
var d = 0

a + b = "420"
c + d = 42

var e = [1, 2]
var f = [3, 4]

e + f = "1,23,4"
  • 한 쪽 피연산자가 문자열이거나 문자열 표현형으로 나타낼 수 있으면 문자열 붙이기를 한다. 그 밖에는 언제나 숫자 덧셈을 한다.
  • 숫자는 공백 문자열 "" 와 더하면 간단히 문자열로 강제변환된다.
  • String() 과의 차이점이 있는데 String()은 toString() 함수를 호출하는 것이고, 공백 문자열 연산은 ToPrimitive 과정에서 값을 valueOf() 메서드에 전달하여 호출하고 그 결괏값이 ToString() 연산을 하여 최종적인 문자열로 변환된다. 객체의 경우 문제가 될 수가 있지만 굳이 일부러 이런 객체를 만들지 않는 이상 크게 문제될 일은 없다.
  • 연산 - 0과 * 1, / 1 연산자는 문자열 -> 숫자로 강제변환을 한다.

4.4.3 암시적 강제변환: 불리언 -> 숫자

  • 범용적인 기법은 아니지만 특정 상황에선 기발한 해법이 될 수 있다.
  • 숫자 + false or true에서 불리언은 숫자로 암시적인 강제변환이 된다.
  • 여기서 논의한 모든 것들에 대한 판단은 우리 몫이다.

4.4.4 암시적 강제변환: * -> 불리언

  • 불리언으로의 강제변환이 일어나는 표현식들
  1. if () 문의 조건 표현식
  2. for ( ; ; )에서 두 번째 조건 표현식
  3. while () 및 do...while () 루프의 조건 표현식
  4. ? : 삼항 연산 시 첫 번째 조건 표현식
  5. || 및 &&의 좌측 피연산자
  • 이런 콘텍스트에서 불리언 아닌 값이 사용되면 불리언 값으로 암시적 강제변환된다.
var a = 42
var b = "abc"
var c
var d = null

if (a) {
  console.log("hi") // hi
}

while (c) {
  console.log("실행 절대 안됨")
}

c = d ? a : b // "abc"

if ((a && d) || c) {
  console.log("hi") // hi
}

4.4.5 &&와 || 연산자

  • 자바스크립트에서 이 두 연산자는 다른 언어와 달리 실제로 결괏값이 논리 값이 아니다.
  • 결괏값은 두 피연산자 중 한쪽 값이다.
var a = 42
var b = "abc"
var c = null

a || b // 42
a && b // "abc"

c || b // "abc"
c && b // null
  • 연산자는 첫 번째 피연산자의 불리언 값을 평가한다. 피연산자가 비 불리언 타입이면 먼저 ToBoolean으로 강제변환 후 평가를 계속한다.
  • || 연산자는 그 결과가 true면 첫 번째 피연산자 값을, false면 두 번째 피연산자 값을 반환한다
  • && 연산자는 true면 두 번째 피연산자의 값을, false면 첫 번째 피연산자의 값을 반환한다.

4.4.6 심벌의 강제변환

  • 심벌 -> 문자열의 암시적 강제변환은 금지되고 에러가 난다.
  • 불리언 값으로는 명시적/암시적 모드 강제변환이 가능하다. (truthy 값)

4.5 느슨한/엄격한 동등 비교

  • 느슨한 동등 비교는 == 연산자를, 엄격한 동등 비교는 === 연산자를 사용함
  • 동등함의 비교시 ==는 강제변환을 허용하지만, ===는 강제변환을 허용하지 않는다.

4.5.1 비교 성능

  • ==는 타입이 다를 경우 강제변환을 해야 하므로 ==가 더 할 일이 많다. 몇 마이크로초 정도 차이가 난다. (백만 분의 1초)
  • 타입이 같은 두 값의 동등 비교라면, ==와 ===의 알고리즘은 동일하다.

4.5.2 추상 동등 비교

  • ES5 11.9.3에 추상적 동등 비교 알고리즘에 강제변환을 어떻게 수행하는지 그 방법이 적혀있다.
  • 객체의 동등 비교에서 ==과 ===는 동일한 로직을 사용한다. 두 객체가 정확히 똑같은 값에 대한 레퍼런스일 경우에만 동등하다.
문자열 -> 숫자
  • 하나가 문자열, 하나가 숫자라면 문자열을 숫자로 비교해서 변환한다.
* -> 불리언
  • 가장 끔찍한 강제변환 함정이 자주 일어나는 곳이다.
  • 숫자 vs 불리언인 경우 불리언을 숫자로 바꿔서 비교한다.
  • 문자 vs 불리언인 경우,
  1. 불리언을 숫자로 먼저 강제 변환
  2. 문자열을 숫자로 강제 변환하여 그 둘을 비교한다.
비교하기: null -> undefined
  • null과 undefined를 느슨한 동등 비교하면 서로에게 타입을 맞춘다. 결과적으로 true가 나온다.
비교하기: 객체 -> 비객체
  • 먼저 객체에 대해 ToPrimitive() 한 결과를 가지고 비교를 한다.
var a = 42
var b = [42]

a == b // true
  • 위 예에서 b가 먼저 문자열 "42"로 바뀌고, 그 둘을 서로 비교한다.
  • null과 undefined는 객체 래퍼가 따로 없으므로 박싱할 수 없다.

4.5.3 희귀 사례

  • 말 그대로 희귀사례. 괜히 내장 함수를 자기 멋대로 바꿔서는 결과가 이상하다고 한탄하지 말자.
  • 책에서 괜히 억지 코드를 만들어서 설명해주는데, 저자도 직접 다 써놓고 "이런 코드는 그 자체로 공해니 생각조차 말고 강제변환을 비난하는 근거로 제시하지도 말자" 라고 설명한다. 말도 안되는 장난은 피하면 그만이라고 한다.
  • 책 p141에 나와있는 나쁜 부분 7인방이라고 저자가 예측하기 어려운 느슨한 동등 비교 7개를 모아놨는데, 본인이 이런 의미 없는 비교는 절대 하지 말자고 써놨다. 애초에 엄격한 동등 비교만 하면 실무에서 이런 일을 겪을 필요가 없다.
암시적 강제변환의 안전한 사용법
  • 피연산자중 하나가 true/false일 가능성이 있으면 '절대로' == 연산자를 쓰지 말자.
  • 피연산자 중 하나가 [], "", 0이 될 가능성이 있으면 가급적 == 연산자를 쓰지 말자.
  • 결론은 엄격한 동등 비교를 사용하면 될 일이다.

4.6 추상 관계 비교

  • 비교 시 피연산자 모두 문자열일 때와 그 외의 경우, 두 가지로 나뉜다.
  1. 먼저 두 피연산자에 대해 ToPrimitive() 강제변환을 실시한다.
  2. 어느 한쪽이라도 문자열이 아닐 경우 양쪽 모두 ToNumber로 강제변환하여 숫자값으로 비교를 한다.
  • 비교 대상이 모두 문자열 값이면, 각 문자를 단순 어휘 비교한다.
var a = [42]
var b = ["43"]

a < b // true
b < a // false

var a = ["42"]
var b = ["043"]

a < b // false

var a = { b: 42 }
var b = { b: 43 }

a < b // false
a <= b // true
a >= b // true
  • a <= b는 실제로 b > a의 평가 결과를 부정하도록 명세에 기술되어 있다. 같거나 더 작은이라는 수학적인 의미가 아니라, 더 크지 않은 (!(a > b))의 의미로 해석한다.
  • 조심해서 관계 비교를 해야할 것 같은 상황에서는 부등호를 사용하기 전에 비교할 값들을 명시적으로 강제변환 해두는 게 낫다.