본문 바로가기

강제변환

본 내용은 카일 심슨의 YOU DON'T KNOW JS 타입과 문법, 스코프와 클로저(2017)를 읽고 서술적인 내용을 Q&A 형식으로 정리한 글입니다.

1. 추상 연산

1.1 자신의 프로토타입에 toString()이 있는 경우(1)와, 일반 객체처럼 Object.prototype.toString() 메소드를 호출하는 경우(2)의 차이점은?

(1) 내장 원시 값은 정해져 있는 방법대로 문자열화되어 반환한다.

(2) 내부 [[Class]]를 반환한다. 정확히는 ToPrimitive 과정을 거친 결과값이 반환되어 "[object Object]"가 반환된다.

ECMA Script 2020 표준 발췌

1.2 JSON.stringify 함수의 각 인자에 대한 설명과 문자열화하는 규칙에 대해 설명하시오.

JSON.stringify(value, function(k, v){ } | [], " ")

  • value: 문자열화할 값
  • function(){ } | []: 대체자, 객체를 재귀적으로 직렬화하여 필터링
    • 배열인 경우, 내부 원소는 문자열이며, 각 원소는 문자열화할 객체(value)의 프로퍼티명
    • 함수인 경우, 처음은 자기자신의 객체에 대해 실행(key는 undefined)하고 그 이후 각 프로퍼티마다 한 번씩 실행되며 key와 value를 인자로 전달
      • undefined를 반환하면 직렬화에서 제외
  • " ": 들여쓰기 문자 정의(기본: 공백Space)
JSON.stringify(["a", "b", "c"], function(k, v){
  if (!k) return v;
  if (k >= 1) return v;
})
/* "[null, "b", "c"]" */

JSON 안전값에 속하지 않는 타입인 경우 (undefined, 함수, Symbol)

배열에 포함되어 있으면 null로 대체하고, 객체에 포함되어 있다면 자동으로 누락함

순환 참조인 경우

에러 발생

1.3 ToNumber(숫자 아닌 값 -> 수식 연산이 가능한 숫자)의 변환 로직에 대해 설명하시오.

7.1.4 ToNumber 발췌

만약 객체가 valueOf 혹은 toString을 커스텀 구현할 시 커스텀된 valueOf(toString)이 타입 변환 시 적용된다.

const a = {
  name: "newjeong",
  age: "27",
  valueOf: function () {
    return this.age;
  }
}

Number(a); // 27

1.4 ToPrimivite란?

해당 객체가 valueOf()를 구현했는지 확인한 후 그렇지 않을 경우에는 toSTring()을 이용하여 강제변환한다. 어떻게 해도 원시 값으로 바꿀 수 없는 경우, TypeError를 throw한다.

 

즉, valueOf와 toString이 없는 객체를 생성하면 강제변환이 불가능한 객체를 생성할 수 있다.

const obj = Object.create(null)

obj.a = 1;

String(obj) // TypeError: Cannot convert object to primitive value

`${obj}` // TypeError: Cannot convert object to primitive value

1.5 ToBoolean 변환 규칙에서 falsy한 값은 무엇인가?

undefined, null, false, +-0, NaN, ""만 ToBoolean 변환 시 false가 되고 이외의 값은 truthy한 값이다.

7.1.2 ToBoolean 발췌

(주의) "0", "false", "''" 처럼 falsy한 값들의 문자열은 truthy한 값이다.

 

2. 명시적 강제변환

2.1 문자열과 숫자간 명시적으로 강제변환하는 법에 대해 모두 설명하시오.

2.1.1 숫자 -> 문자열

String()

2.1.2 문자열 -> 숫자

  • Number() : Toumber 추상 연산 로직을 따름
  • 문자열 앞에 +, - 단항 연산자  (일반 연산자와 함께 쓰일 시 혼동하기 쉬우므로 다른 연산자와의 사용은 지양)
  • 날짜: Date.now()(ES5~), new Date().getTime(), +new Date()
  • ~(틸드) : ToInt32 숫자로 강제변환 후 NOT 연산 (비트 뒤집기),즉 2의 보수를 구할 수 있음
    • ~x === -(x+1)
  • parseInt() : 파싱 도중 숫자 변환 불가능한 문자를 만나면 변환 멈춤
Number("12a") // NaN

parseInt("12a") // 12

2.2 ~(틸드)의 용도는?

  • 용도 1. -1 경계값에 틸드 적용 시 0(falsy), 나머지 값들은 1(truthy)인 점을 활용한 indexOf() 의 '구멍난 추상화' 숨기기
  • 용도 2. ~~(더블 틸트)로 숫자의 소수점을 제거(=== 양수에 한하여 Math.floor(), 음수의 경우 Mah.ceil())
    • 상위 비트를 잘라내는 ~~보다 x | 0로 하위 비트를 잘라내는게 미미하게 더 빠를 수 있지만 연산자 우선순위에 따라 ~~를 사용해야 하는 경우가 존재
const str = "newjeong is hungry. she wants to eat dessert such as cookie..."

if (~str.indexOf("hungry")) { // Exist
  return 1
}

2.3 parseInt()의 동작 방식에 대해 설명하시오.

첫번째 인자가 비문자열인 경우, 문자열로 강제변환한 후 숫자 파싱을 진행

두번째 인자는 기수를 설정하여 진법 종류를 설정할 수 있음

두번째 인자를 주지 않을 경우,

  • ES5 이전: 첫번째 인자의 문자열이 x로 시작하면 16진수, 0이면 8진수로 임의 해석
  • ES5~ : "0x"는 16진수로, 나머지는 10진수로 해석

 

ES5 이전에서는 다음의 오류를 주의한다.

const hour = parseInt("07");
const minute = parseInt("05");

console.log("%d:%d", hour, minute); // "0:0"

비문자열 파싱

parseInt()는 내부에 비문자열이 인자로 들어올 경우, 문자열로 변환 후 진행

parseInt(1/0, 19) // "Infinity" -> 19진수 -> 18
parseInt(new String("1")) // "1"로 언박싱 -> 1
parseInt(false, 16) // "false" -> 16진수 -> 250
parseInt(0.00008) // "0.00008" -> 0
parseInt(0.00000008) // "8e-8" -> 8
parseInt(parseInt, 16) // "function ..." -> 16진수 -> 15

2.4 불리언 강제 변환

Boolean(x), !!x

 

3. 암시적 강제변환

3.1 문자열과 숫자 간의 암시적 강제변환에 대해 설명하시오.

사칙연산자로 암시적 강제변환이 가능하다.

+연산자는 인자 중 하나라도 문자열이 있거나 문자열로 변환 가능한 경우, 문자열로 강제변환 후 + 알고리즘을 수행한다.

const a = 1
const b = a + "" // "1"

const arr1 = [1,2,"new"]
const arr2 = ["jeong",3,4]

const arr3 = arr1 + arr2 // "1,2,newjeong,3,4"

 

[참고] 문자열로 변환 가능한 경우란, toPrimitive 로직을 거쳐 valueOf()과 toString()으로 문자열 변환이 가능한 경우를 말한다.
따라서 객체에 valueOf 혹은 toString을 커스텀 구현하면 예상치 못한 결과를 얻을 수 있으니 주의한다.
const a {
  name: "newjeong",
  age: 27
}

const b = a + "" // "[object Object]"

a.valueOf = function() {
  return this.age
}

const d = a + "" // "27"

- , *, /연산자는 반드시 숫자 연산만 가능하므로 문자열을 숫자로 강제변환하고 싶을 때 활용한다.

const a = "27" - 0 // 27

const arr1 = [100]
const arr2 = [1]

const num = arr1 - arr2 // 99

3.2 인자로 받은 값들 중 하나라도 true/truthy라면 true를 반환하는 함수를 작성하시오.

function hasTruthy(...args) {
  for (const v of args) {
    if (!!v) {
      return !!v
    }
  }
  return false
}

const a=true,b=false,c=0,d=""

hasTruthy(a,b,c,d) // true
hasTruthy(b,c,d) // false

3.3 암시적으로 Boolean으로 강제변환되는 표현식을 열거하시오.

  • if () 조건문
  • for( ; ; )문의 두번째 조건 표현식
  • while(), do {...} while() 문의 조건 표현식
  • 삼항 연산자의 조건 표현식
  • ||, && 논리 연산자의 좌측 피연산자

3.4 &&, || 논리 연산자의 동작 방식에 대해 설명하시오.

좌측 피연산자를 Boolean으로 암시적 강제변환했을 때 결과값이 true/false인지에 따라 첫번째 혹은 두번째 피연산자의 평가값을 선택한다.

const a = "newjeong"
const b = "juj"
const c = null

/**
  * 첫번째 피연산자가 true라면 두번째 피연산자의 결과값을 선택한다. 
  * 반면, 첫번째 피연산자가 false라면 첫번째 피연산자의 결과값을 선택한다.
*/
a && b // "juj"
c && b // null

/**
  * 첫번째 피연산자가 true라면 첫번째 피연산자의 결과값을 선택한다. 
  * 반면, 첫번째 피연산자가 false라면 두번째 피연산자의 결과값을 선택한다.
*/
a || b // "newjeong"
c || b // "juj"
[참고] 어떻게 보면 a && b는 a? b : a로, a || b는 a? a : b로 해석할 수 있다.
다만 삼항 연산자는 a가 두 번 평가되는 반면, a && b에서 a는 단 한번만 평가된 후 ToBoolean 추상 연산에 따라 Boolean으로 강제변환된다.

&& 연산자의 활용

&& 연산자는 '가드 연산자'로, 어떤 값이 truthy(true)라면 해당 값을 인자로 하는 함수를 호출하는 등 추가 로직을 수행할 수 있다. 이를 '단락 평가'라고 부른다.

const sayHello = (name) => {
  console.log(`Hello, ${name}!`)
}

const a = "newjeong"

a && sayHello(a)

|| 연산자의 활용

첫번째 피연산자가 falsy일 때 무조건 두번째 피연산자를 평가하는 것이 보장된 로직에서 활용한다.

const getName = (firstName, lastName) => {
  const fName = firstName || "Jane"
  const lName = lastName || "Doe"
  
  return `${fName} ${lName}`
}

getName("Newjeong", "Jeon") // "Newjeong Jeon"
getName() // "Jane Doe"
getName("Zoey", "") // "Zoey Doe"

 

4. 느슨한(==) / 엄격한(===) 동등 비교

질문으로 넘어가기 전에 ...

ECMA Script 2020 표준 명세에서 말하는 Abstract Equality Comparison (==) 의 동작방식에 대해 기술해보았다.

더보기

7.2.15 Abstract Equality Comparison

The comparison x == y, where x and y are values, produces true or false. Such a comparison is performed as follows:

1. If Type(x) is the same as Type(y), then
    a. Return the result of performing Strict Equality Comparison x === y.

    7.2.16의 Strict Equality Comparison(아래 '더보기')을 수행

2. If x is null and y is undefined, return true.

3. If x is undefined and y is null, return true.

4. If Type(x) is Number and Type(y) is String, return the result of the comparison x == ! ToNumber(y).

5. If Type(x) is String and Type(y) is Number, return the result of the comparison ! ToNumber(x) == y.

6. If Type(x) is BigInt and Type(y) is String, then

    a. Let n be ! StringToBigInt(y).
    b. If n is NaN, return false.

    NaN은 자기자신과도 동등하지 않다.
    c. Return the result of the comparison x == n.

7. If Type(x) is String and Type(y) is BigInt, return the result of the comparison y == x.

8. If Type(x) is Boolean, return the result of the comparison ! ToNumber(x) == y.

9. If Type(y) is Boolean, return the result of the comparison x == ! ToNumber(y).

10. If Type(x) is either String, Number, BigInt, or Symbol and Type(y) is Object, return the result of the comparison x == ToPrimitive(y).

11. If Type(x) is Object and Type(y) is either String, Number, BigInt, or Symbol, return the result of the comparison ToPrimitive(x) == y.

12. If Type(x) is BigInt and Type(y) is Number, or if Type(x) is Number and Type(y) is BigInt, then

    a. If x or y are any of NaN, +∞, or -∞, return false.

    b. If the mathematical value of x is equal to the mathematical value of y, return true; otherwise return

false.

13. Return false.

다음은 엄격한 동등비교(===)의 매커니즘이다.

더보기

7.2.16 Strict Equality Comparison

The comparison x === y, where x and y are values, produces true or false. Such a comparison is performed as follows:

1. If Type(x) is different from Type(y), return false.

2. If Type(x) is Number or BigInt, then

    a. Return ! Type(x)::equal(x, y).

    +0과 -0은 다른 값으로 인식되어 false를 반환
3. Return ! SameValueNonNumeric(x, y).

 

※ SameValueNonNumeric

The internal comparison abstract operation SameValueNonNumeric(x, y), where neither x nor y are numeric type values, produces true or false. Such a comparison is performed as follows:

1. Assert: Type(x) is not Number or BigInt.

2. Assert: Type(x) is the same as Type(y).

3. If Type(x) is Undefined, return true.
4. If Type(x) is Null, return true.

5. If Type(x) is String, then
    a. If x and y are exactly the same sequence of code units (same length and same code units at

corresponding indices), return true; otherwise, return false.

6. If Type(x) is Boolean, then

    a. If x and y are both true or both false, return true; otherwise, return false.

7. If Type(x) is Symbol, then

    a. If x and y are both the same Symbol value, return true; otherwise, return false.

8. If x and y are the same Object value, return true. Otherwise, return false.

    두 객체가 같은 객체 레퍼런스일 때만 true를 반환한다. 

4.1 숫자와 문자열의 느슨한 동등 비교(==)는?

4. If Type(x) is Number and Type(y) is String, return the result of the comparison x == ! ToNumber(y).
5. If Type(x) is String and Type(y) is Number, return the result of the comparison ! ToNumber(x) == y.

문자열이 숫자로 변환됨

4.2 Boolean과의 비교

8. If Type(x) is Boolean, return the result of the comparison ! ToNumber(x) == y.
9. If Type(y) is Boolean, return the result of the comparison x == ! ToNumber(y).

Boolean이 숫자로 변환됨

따라서 "42"라는 문자열은 truthy한 값이지만 Boolean이 숫자로 변환되어 0 또는 1로 변환되기 때문에 ==와 전혀 관련이 없다.

이는 오해의 소지가 다분하여 ==의 사용을 지양하고 강제변환이 일어나지 않는 === 사용(혹은 !!을 통한 명시적 변환)을 권장한다.

"42" == true // false
"42" == false // false

4.3 null == undefined의 결과는?

true

2. If x is null and y is undefined, return true.
3. If x is undefined and y is null, return true.

4.4 객체와 비객체의 느슨한 동등비교(==)는?

10. If Type(x) is either String, Number, BigInt, or Symbol and Type(y) is Object, return the result of the comparison x == ToPrimitive(y).
11. If Type(x) is Object and Type(y) is either String, Number, BigInt, or Symbol, return the result of the comparison ToPrimitive(x) == y.
const a = 42
const b = [42]

a == b // true. 42 === ToPrimitive([42]) -> "42" -> 42 

이는 마치 객체 Wrapper의 언박싱을 통해 스칼라 값으로 강제변환하는 것과 같은 이치이다.

const a = "newjeong"
const b = Object(a) // new String(a)
a == b // true

단, null, undefined의 객체 Wrapper는 Object()가 되어 null == {}의 결과인 false가 반환된다.

NaN의 경우도 마찬가지로 NaN == NaN은 false임을 유의한다.

4.5 다음 느슨한 동등비교(==)의 결과는?

// (1)
"0" == false

// (2)
false == 0

// (3)
false == ""

// (4)
false == []

// (5)
"" == 0

// (6)
"" == []

// (7)
0 == []

// (8)
[] == ![]

(1) true ("0" ⇢ 0, false ⇢ 0)

(2) true (false ⇢ 0)

(3) true (false ⇢ 0, "" ⇢ 0)

(4) true (false ⇢ 0, [] ⇢ ToPrimitive([]) ⇢ "" ⇢ 0)

(5) true ("" ⇢ 0)

(6) true ("" ⇢ 0, [] ⇢ ToPrimitive([]) ⇢ "" ⇢ 0)

(7) true ([] ⇢ ToPrimitive([]) ⇢ "" ⇢ 0)

(8) true ([] ⇢ ToPrimitive([]) ⇢ "" ⇢ 0, ![] ⇢ false ⇢ 0)

 

위의 긍정오류처럼 false인 거 같지만 true인 비교를 주의한다.

예를 들어 func(a, b) { return a == b }에서 func(0, []) 을 넘기는 것처럼...

책에서는 다음 경우에 느슨한 동등비교의 사용을 주의하라고 한다.
- 피연산자중 하나가 true/false일 가능성이 있다면 절대로 ==을 사용하지 않는다.
- 피연산자중 하나가 [], "", 0이 될 가능성이 있다면 가급적 ==을 사용하지 않는다.

4.6 추상 관계 비교(<, >)는 어떻게 동작하는가?

피연산자가 모두 문자열인 경우, 각 문자 순서대로 어휘 비교를 수행한다.

"newjeong" > "jeon" // true

피연산자 중 하나라도 문자열이 아닌 경우, ToPrimitive() 연산을 하여 강제변환한다.

[42] < ["43"] // true, 42 < 43
[42] < ["aaa"] // true, "42" < "aaa"

<=, >=는 사실 <, >의 NOT 연산이다.

const val1 = {b: 10}
const val2 = {b: 10}

val1 < val2 // false
val1 <= val2 // true

추상 관계 비교는 강제 변환을 금할 수 없기 때문에 가급적 명시적 강제변환을 통해 비교한다.

 

'Books > YOU DON'T KNOW JS' 카테고리의 다른 글

스코프와 클로저  (0) 2020.10.05
문법  (0) 2020.09.08
타입, 값 그리고 네이티브  (0) 2020.08.04