심오한 자바스크립트 형변환의 세계

Lybell·2022년 8월 26일
0

시작하기 전에

(출처 : https://dev.to/coderslang/javascript-interview-question-34-different-ways-to-get-the-current-date-in-js-4c25)

우선 시작하기 전에 위 사진의 문제를 풀어보자. 답은 true일까, false일까? 답은 이 글 맨 마지막에 알려주겠다.

명시적 형변환과 암시적 형변환

자바스크립트는 약타입 언어다. 그래서 "123" + 4 같은 것을 처리할 때 다른 강타입 언어는 타입이 달라서 오류를 뱉겠지만, 자바스크립트는 어떻게든 형변환해서 처리한다. 이 때, 프로그래머가 명시적으로 값의 타입을 변환시키면(Number(123) 같은 경우) 명시적 형변환이라고 하고, 그렇지 않고 자바스크립트 엔진이 알아서 형변환하도록 하는 것을 암시적 형변환이라고 한다.

원시 자료형의 형변환

원시 자료형에는 Number, String, Boolean, null, undefined, Symbol(ES6부터), BigInt(ES2020부터)가 존재한다. 이 중 가장 많이 쓰이고 많이 변환되는 문자열, 숫자, 불리언 자료형으로의 형변환에 대해 알아보자.

toString(문자열 형변환)

"Lybell".concat(123)은 무엇을 반환해야 할까? 문자열에 숫자나 불리언 등을 결합하고 싶을 때 등의 상황에서, 자바스크립트는 암묵적으로 문자열이 아닌 자료형을 문자열로 형변환하는 toString 추상 연산을 호출한다.

ECMAScript 명세서에서 정의하고 있는 각 자료형의 문자열 형변환 규칙은 다음과 같다.

자료형결과
undefined"undefined"
null"null"
Booleantrue일 때 "true", false일 때 "false"
NumberNumber.toString(value, 10)
String문자열 그 자체
SymbolTypeError를 반환
BigIntBigInt.toString(value)
ObjecttoPrimitive(value, "string")으로 원시 자료형으로 만든 뒤 문자열로 형변환

명시적으로 문자열로 형변환을 하기 위해서는 템플릿 리터럴을 사용하거나, String() 함수를 이용하면 된다.(new String()을 사용하면 문자열처럼 보이는 객체가 생성되므로 주의!) value+""와 같은 방법도 존재하나, preferedType이 string이 아니라 default이고, 객체를 형변환할 때 valueOf@@Symbol.toPrimitive 메소드가 다른 리터럴을 반환한다면 그것을 기준으로 문자열로 형변환되기 때문에 원하는 값을 얻지 못할 수 있다.

//String() 함수
console.log(String(123)) // 문자열 123
console.log(String(true)) // 문자열 "true"

//템플릿 리터럴
console.log(`${123}`) // 문자열 "123"
console.log(`${10000000000n}`) // 문자열 "10000000000"
console.log(`${undefined}`) // 문자열 "undefined"
console.log(`${null}`) // 문자열 "null"
console.log(`${Symbol()}`) // TypeError
console.log(`${[1,2,3]}`) // 문자열 "1,2,3"
console.log(`${{a:12}}`) // 문자열 "[Object Object]"

//value+""로 문자열 이어붙이기
console.log(123+""); // 문자열 "123"
console.log(null+""); // 문자열 "null"

//객체의 경우
const test = {valueOf:()=>300, toString:()=>"yes"};
console.log(`${test}`); // 문자열 "yes"
console.log(String(test)); // 문자열 "yes"
console.log(test+""); // 문자열 "300"?!

toNumber(숫자 형변환)

123 - "45"는 무엇을 반환해야 할까? 숫자와 숫자가 아닌 것을 연산하거나 비교해야 할 때, 자바스크립트는 암묵적으로 숫자가 아닌 것을 숫자로 형변환하는 toNumber 추상 연산을 호출한다.

ECMAScript 명세서에서 정의하고 있는 각 자료형의 숫자형 형변환 규칙은 다음과 같다.

자료형결과
undefinedNaN
null0
Booleantrue일 때 1, false일 때 0
Number숫자 그 자체
String내부 연산 StringToNumber(value)를 호출한다.
SymbolTypeError를 반환
BigIntTypeError를 반환(toNumeric에서는 숫자 그 자체를 반환)
ObjecttoPrimitive(value, "number")으로 원시 자료형으로 만든 뒤 숫자형으로 형변환

문자열이 숫자로 형변환될 때 적절하지 않은 포맷(예시:123a)이면 NaN을 반환하며, 공백만 있는 문자열이 0을 반환한다는 것을 기억하도록 하자.

여기에서 빈 배열이 0으로 형변환되는 이유를 알 수 있는데, 빈 배열은 toPrimitive(value, "number")를 호출하면 [].toString()의 값인 빈 문자열("")이 원시 자료형으로 반환되고, 이것을 숫자형으로 파싱하면 0이 되기 때문에 빈 배열이 0으로 초기화되는 것이다.

명시적으로 숫자형으로 형변환을 하기 위해서는 Number() 함수를 사용하거나, unary plus 연산자를 이용하면 된다. 단, Number() 함수는 BigInt형의 값 역시 잘 변환할 수 있기 때문에, 완벽한 toNumber는 아니다.

//Number() 함수
console.log(Number("123")) // 숫자 123
console.log(Number("123.45")) // 숫자 123.45
console.log(Number("123p")) // NaN
console.log(Number(" ")) // 숫자 0
console.log(Number(1234n)) // 숫자 1234

//unary plus 연산자
console.log(+"1024") // 숫자 1024
console.log(+undefined) // NaN
console.log(+null) // 숫자 0
console.log(+Symbol()) // TypeError
console.log(+1024n) // TypeError
console.log(+true) // 숫자 1
console.log(+[]) // 숫자 0
console.log(+[1,2,3]) // NaN
console.log(+{}); // NaN

//객체의 경우
const test = {valueOf:()=>300, toString:()=>"yes"};
console.log(+test); // 숫자 300
const test2 = {toString:()=>""};
console.log(+test2); // 숫자 0

toNumeric

내부 연산 toNumber와 비슷하지만, toNumeric은 원시 자료형으로 만든 뒤 그 값이 BigInt형이면 BigInt형 값 그 자체를 반환한다.

toBoolean(불리언 형변환)

조건문에서 true나 false가 아닌 것이 들어갔을 때 어떻게 처리해야 할까? 조건문 등 불리언 값이 필요한 상황에서 불리언 값이 아닌 값이 들어갔을 때, 자바스크립트는 암묵적으로 불리언 자료형이 아닌 것을 불리언 값으로 형변환하는 toNumber 추상 연산을 호출한다.

ECMAScript 명세서에서 정의하고 있는 각 자료형의 불리언 형변환 규칙은 다음과 같다.

자료형결과
undefinedfalse
nullfalse
Boolean불리언 값 그 자체
Number0, NaN이면 false, 그 외에는 true
Stringlength가 0이면(빈 문자열) false, 그 외에는 true
Symboltrue
BigInt0n이면 false, 그 외에는 true
Objecttrue

Object는 빈 배열이든, 빈 객체든, 심지어 new Boolean(false)와 같은 값이 false인 불리언 래퍼 객체든 무조건 true로 형변환된다는 사실에 유의하자.

명시적으로 불리언으로 형변환을 하기 위해서는 Boolean() 함수를 사용하거나, !! 연산자를 이용하면 된다. !! 연산자는 !으로 값을 형변환한 뒤 불리언 값을 뒤집고, 다시 !을 붙이면 그 값이 뒤집어져 최종적으로 값이 불리언으로 형변환되는 원리다.

//Boolean() 함수
console.log(Boolean(123)) // true
console.log(Boolean(0)) // false
console.log(Boolean(NaN)) // false
console.log(Boolean(" ")) // true

//!! 연산자
console.log(!!123) // true
console.log(!!"yes") // true
console.log(!!" ") // true
console.log(!!"") // false
console.log(!!Symbol()) // true
console.log(!!1024n) // true
console.log(!!null) // false
console.log(!!undefined) // false
console.log(!![]) // true
console.log(!!{}) // true
console.log(!!(new Boolean(false)) )// true

Object의 형변환

Object 자료형은 원시 자료형으로 변환하기 위해 특수한 형변환 과정을 거친다. ECMAScript 명세서에서 정의하고 있는 Object의 원시 자료형 형변환은 다음과 같다. 이해를 돕기 위해 자바스크립트로 짠 toPrimitive 함수를 같이 첨부한다.

function isPrimitive(object)
{
  if(object === null) return true;
  return (typeof object !== "object" && typeof object !== "function");
}
function assertHint(preferedType)
{
  if(preferedType === undefined) return "default";
  if(preferedType === "string") return "string";
  return "number";
}

function toPrimitive(object, preferedType)
{
  if(isPrimitive(object)) return object;
  if(Symbol.toPrimitive in object)
  {
    let hint = assertHint(preferedType);
    let primitive = object[Symbol.toPrimitive](hint);
    if(isPrimitive(primitive)) return primitive;
    throw new TypeError("Cannot convert object to primitive value");
  }
  return toOrdinaryPrimitive(object, preferedType);
}

function toOrdinaryPrimitive(object, hint="number")
{
  let methodNames;
  if(hint === "string") methodNames=["toString", "valueOf"];
  else methodNames=["valueOf", "toString"];
  for(let name of methodNames)
  {
    if(!(name in object)) continue;
    if(typeof object[name] !== "function") continue;
    let primitive = object[name]();
    if(isPrimitive(primitive)) return primitive;
  }
  throw new TypeError("Cannot convert object to primitive value");
}
  1. 만약 원시 자료형이면 값을 그대로 반환한다.
  2. @@Symbol.toPrimitive 메소드의 존재 여부를 파악한다.
  3. 만약 @@Symbol.toPrimitive 메소드가 존재하면, preferedType을 통해 hint를 추측한다.
    • preferedType이 정해지지 않으면 기본적으로 default
    • preferedType이 string이면 string
    • 그 외에는 number
  4. 만약 @@Symbol.toPrimitive 메소드가 존재하면, [Symbol.toPrimitive](hint) 메소드를 호출한다. 그 값이 원시 자료형이면 값을 반환하고, 그렇지 않으면 오류를 반환한다.
  5. valueOf와 toString을 기반으로 형변환을 진행한다. 이 때 preferedType이 무엇인지에 따라 다르다.
    • preferedType이 string이면 toString, valueOf 우선
    • 그 외에는 valueOf, toString 우선
  6. 각 methodName에 대해 형변환을 진행한다. 이 때 해당하는 메소드가 존재하지 않거나, 해당하는 식별자가 메소드가 아니면 건너뛴다. 반환값이 원시 자료형이면 값을 반환한다.
  7. 만약 @@Symbol.toPrimitive, toString, valueOf 모두 존재하지 않으면 Uncaught TypeError: Cannot convert object to primitive value 에러를 반환한다.

@@Symbol.toPrimitive

Symbol.toPrimitive 심볼은 ES6부터 추가된 원시 자료 형변환 메소드로, valueOftoString보다 우선된다. valueOftoString과는 다르게 Object.prototype에는 @@Symbol.toPrimitive 메소드가 정의되어 있지 않다.

@@Symbol.toPrimitive 메소드는 하나의 매개변수를 가지며, 그 인자의 값은 반드시 "string", "number", "default" 중 하나가 와야 한다. 리턴값은 반드시 원시 자료형이어야 하고, 위에서 보았듯이 객체 자료형이면 에러가 뜬다. 사용자가 이 메소드를 정의하고자 한다면 hint값에 따라 값이 달라지는 구조로 코드를 작성해야 한다.

다음의 예제를 보자.

let example = {
  [Symbol.toPrimitive](hint){
    if(hint === "number") return 123;
	if(hint === "string") return '456';
    return 789;
  }
}
console.log(+example); // unary plus는 preferType이 number이므로 123이 반환됨
console.log(example*1); // -, * /, %는 preferType이 number이므로 123이 반환됨

console.log(`${example}`); // 템플릿 문자열은 preferType이 string이므로 '456'이 반환됨
console.log("".concat(example)); 
// 문자열의 concat 메소드는 preferType이 string이므로 '456'이 반환됨

console.log(0 + example); // + 연산자는 preferType이 default이므로 789가 반환됨
console.log(789 == example); // == 연산자는 preferType이 default이므로 789가 반환됨

valueOf

valueOf 메소드는 원시 자료 형변환 메소드로, 원시 자료형을 반환하는 데에 쓰인다. 객체도 반환할 수 있으나, 객체가 반환되면 암시적 형변환에서 무시된다. 많은 사람들이 valueOf는 숫자만 반환할 수 있다고 오해하지만, 그렇지 않다. 객체를 대표할 수 있는 원시 자료형이면 뭐든지 반환할 수 있다.(그래도 숫자를 반환하는 것이 권장된다.)

기본적으로 Object.prototype에는 valueOf 메소드가 정의되어 있으며, 그 값은 자기 자신을 반환한다. 그래서 일반적인 객체는 toString을 기반으로 원시 자료형으로 변경되게 된다. Date 객체의 valueOf 메소드는 1970년 1월 1일 0시 0분 0.0초부터 지금까지 걸린 시간을 숫자형으로 반환한다.

객체를 대표하는 숫자같은 것을 반환한다는 점에서 unary plus 연산자와도 비슷함을 느낄 수 있으나, unary plus 연산자가 평가될 때 valueOf 메소드가 toString보다 우선될 뿐 대체가 불가능하다.

toString

toString 메소드는 원시 문자열 형변환 메소드로, 객체를 대표하는 문자열을 반환하는 데에 쓰인다. 사실 toString 역시 문자열이 아는 것들이 반환될 수 있으나, 웬만해서는 문자열을 반환해야 한다.

기본적으로 Object.prototype에는 toString 메소드가 정의되어 있으며, 그 값의 결정 방식은 다음과 같다.

  1. caller가 undefined이면 [Object Undefined]를 반환한다.
  2. caller가 null이면 [Object Null]을 반환한다.
  3. caller가 원시 자료형이면 이를 래퍼 객체로 변환한다.
  4. builtinTag를 결정한다. 결정 방식은 다음과 같다.
    • caller.isArray() === true이면 Array
    • caller가 [[ParameterMap]] 내부 슬롯이 있으면(=argument 객체) Arguments
    • caller가 [[Call]] 내부 메소드가 있으면(=함수) Function
    • caller가 [[ErrorData]] 내부 슬롯이 있으면(=Error 객체) Error
    • caller가 [[BooleanData]] 내부 슬롯이 있으면(=불리언) Boolean
    • caller가 [[NumberData]] 내부 슬롯이 있으면(=숫자형) Number
    • caller가 [[StringData]] 내부 슬롯이 있으면(=문자열) String
    • caller가 [[DateValue]] 내부 슬롯이 있으면(=Date 객체) Date
    • caller가 [[RegExpMatcher]] 내부 슬롯이 있으면(=정규 표현식) RegExp
    • 그 외의 경우 Object
  5. caller에 @@Symbol.toStringTag 프로퍼티가 존재하고, 그것이 문자열인지 확인한다. 만약 그렇다면 tag는 caller[Symbol.toStringTag]로 정해지고, 그렇지 않으면 builtinTag로 결정된다.
  6. 문자열 [Object (tag)]를 반환한다.

ES6부터 추가된 Map, Set, Promise 등의 객체는 각 객체의 프로토타입에 @@Symbol.toStringTag가 설정되어 있으므로, 따로 빌트인 태그 결정 방식을 변경하지 않아도 무방하다.

Function.prototype과 Array.prototype에는 별도의 toString 메소드가 오버라이딩되어 있으며, Object.prototype.toString의 로직을 따르지 않는다. 대신, 다음과 같이 call 메소드를 사용함으로써 Object.prototype.toString의 로직을 따르게 변경할 수 있다.

console.log(String(function(a,b){return a+b})); // "function(a,b){return a+b})"
console.log(String([1,2,3])) // "1,2,3"
console.log(Object.prototype.toString.call([1,2,3])) // "[Object Array]"

각 연산별 형변환 규칙

산술 연산자, 비트 연산자(+ 제외)

산술 연산자(-, *, /, %, **)와 비트 연산자(|, &, ^, <<, >>, >>>)는 추상 연산으로 ApplyStringOrNumericBinaryOperator라는 연산을 이용하여 값을 계산한다. 값을 계산할 때에는 기본적으로 숫자형/BigInt형으로 변환되며, 객체를 원시 자료형으로 변환할 때 preferType은 number로 정해진다.(@@Symbol.toPrimitive("number"), valueOf, toString 순으로 우선순위를 가짐)

ECMAScript 명세서에서 정의하고 있는 산술 연산자의 알고리즘은 다음과 같다. 이해를 돕기 위해 자바스크립트로 짠 binaryOperator 함수를 같이 첨부한다.

function toNumeric(value)
{
  const numeric = toPrimitive(value, "number");
  if(typeof numeric === "bigint") return numeric;
  return +numeric;
}
function binaryOperator(lval, operator, rval)
{
  const lnum = toNumeric(lval);
  const rnum = toNumeric(rval);
  if(typeof lnum !== typeof rnum) {
  throw new TypeError("Cannot mix BigInt and other types, use explicit conversions");
  }
  if(typeof lnum === "bigint") return bigIntOperator(lnum, operator, rnum);
  return numberOperator(lnum, operator, rnum);
}
  1. 좌항과 우항에 각각 toNumeric 추상 연산을 적용하여 숫자형/bigInt형으로 만든다.
    • 이때 각 항이 객체이면 preferType을 number로 설정하여 원시 자료형으로 변환한다.
  2. 만약 좌항과 우항이 다른 타입이면 TypeError를 반환한다.
  3. 연산자를 기반으로 좌항과 우항을 연산한다. 연산자에 대한 설명은 생략한다.

산술 연산자 +

산술 연산자 중 + 연산자는 문자열 연결 연산자의 역할로도 사용할 수 있기 때문에, 다른 산술 연산자와는 다른 양상을 띈다. 그것은 객체를 원시 자료형으로 변경할 때 preferType이 default로 정해진다는 것이다. 즉, 객체에 @@Symbol.toPrimitive가 존재하면 인자로 "default"가 들어간 값이 출력된다.

ECMAScript 명세서에서 정의하고 있는 + 연산자의 알고리즘은 다음과 같다. 이해를 돕기 위해 자바스크립트로 짠 plusOperator 함수를 같이 첨부한다.

function plusOperator(lval, rval)
{
  const leftPrim = toPrimitive(lval); // preferType default
  const rightPrim = toPrimitive(rval);
  if(typeof leftPrim === "string" || typeof rightPrim === "string") {
    const leftStr = toString(leftPrim);
    const rightStr = toString(rightPrim);
    return leftStr.concat(rightStr);
  }
  return binaryOperator(leftPrim, "+", rightPrim);
}
  1. 좌항과 우항에 각각 toPrimitive 추상 연산을 적용하여 원시 자료형으로 만든다.
    • 이때 각 항이 객체이면 preferType을 default로 설정하여 원시 자료형으로 변환한다.
  2. 만약 원시 자료형으로 변한 좌항 또는 우항의 타입이 문자열이면,
    1. 변환된 원시 자료형 값들을 문자열로 형변환한다.
    2. 좌항에 우항을 연결한 문자열을 반환한다.
  3. 그렇지 않으면, 일반적인 산술 연산자의 알고리즘을 따른다. 이때 실질적으로 계산하는 값으로 취급되는 것은 preferType이 default로 설정된 원시 자료형이지, preferType이 number로 취급되는 것이 아님에 주의하자.

비교 연산자(부등호)

ECMAScript 명세서에서 비교 연산자는 특이하게 정의된다. 모두 같은 기본 추상 연산인 IsLessThan(left, right, leftFirst)를 사용하기 때문에, 순서와 최종 리턴값이 달라진다. 그 표는 다음과 같다.

표현식isLessThan 호출리턴값실제 계산되는 연산식
left < rightisLessThan(left, right, true)undefined일 시 false, 그 외 결과값left < right
left <= rightisLessThan(right, left, false)undefined, true일 시 false, false일 시 true!(right < left)
left > rightisLessThan(right, left, false)undefined일 시 false, 그 외 결과값right < left
left >= rightisLessThan(left, right, true)undefined, true일 시 false, false일 시 true!(left < right)

isLessThan에 세 번째 연산자로 left가 right보다 실제로 더 왼쪽에 있는가를 표시하는데, 그 이유는 자바스크립트는 왼쪽에서 오른쪽으로 값을 평가하고, 형변환 과정에서 생기는 잠재적인 부수 효과가 왼쪽에서 오른쪽으로 평가되는 것처럼 보이게 하기 위해서이다.(객체의 원시 자료형 변환은 함수를 통해 이루어지기 때문에, 해당 함수에 부수효과가 존재하면 원시 자료형 변환에 부수효과가 존재한다.)

ECMAScript 명세서에서 정의하고 있는 비교 연산자의 형변환 과정은 다음과 같다. 이해를 돕기 위해 자바스크립트로 작성한, a<b인 상황을 기준으로 한 isLessThen(left, right)의 코드를 같이 첨부한다.

function _BigInt(value)
{
  try {
    return BigInt(value);
  }
  catch {
    return undefined;
  }
}
function oneIsBigInt_and_OtherIsString(left, right)
{
  if(typeof left === "string" && typeof right === "string") return true;
  if(typeof left === "bigint" && typeof right === "string") return true;
  return false;
}
// 문자열 비교 함수
function stringIsLessThen(left, right)
{
  const leftLength = left.length;
  const rightLength = right.length;
  for(let i=0; i<Math.min(leftLength, rightLength); i++)
  {
    const leftChar = left.charCodeAt(i);
    const rightChar = right.charCodeAt(i);
    if(leftChar < rightChar) return true;
    if(leftChar > rightChar) return false;
  }
  return leftLength < rightLength;
}
// 숫자형 비교 함수
function numberIsLessThen(left, right)
{
  if(Number.isNaN(left) || Number.isNaN(right)) return undefined;
  if(left === +0 && right === -0) return false;
  if(left === -0 && right === +0) return true;
  if(left === Infinity || right === -Infinity) return false;
  if(left === -Infinity || right === Infinity) return true;
  return left < right;
}

function _isLessThen(left, right)
{
  //좌우항을 원시 자료형으로 변환. 
  /*
  실제 자바스크립트에서는 a<b뿐만 아니라 a>b 같은 것도 IsLessThan 추상 연산을 쓰기 때문에, 
  isLessThen(b,a) 같은 것은 right를 먼저 원시 자료형으로 변경시킴으로써 
  부수효과의 순서를 맞출 필요가 있다.
  */
  const leftPrim = toPrimitive(left, "number");
  const rightPrim = toPrimitive(right, "number");
  
  //원시 좌우항이 모두 문자열이면 문자열 비교 수행
  if(typeof leftPrim === "string" && typeof rightPrim === "string") {
     return stringIsLessThen(leftPrim, rightPrim);
  }
  
  //한쪽이 BigInt형이고 다른 한쪽이 문자열이면 문자열을 BigInt형으로 변환 후 비교
  if(oneIsBigInt_and_OtherIsString(leftPrim, rightPrim)) {
    const leftBigInt = _BigInt(leftPrim);
    const rightBigInt = _BigInt(rightPrim);
    if(leftBigInt === undefined || rightBigInt === undefined) return undefined;
    return leftBigInt < rightBigInt;
  }
  
  //좌우항을 숫자형/bigInt형으로 변환
  /*
  toNumeric의 대상이 되는 leftPrim과 rightPrim은 원시 자료형이며,
  원시 자료형끼리의 형변환은 자바스크립트 내부에서 취급하기 때문에
  부수효과가 일어나지 않는다. 따라서 순서를 바꿔서 처리할 필요가 없다.
  */
  const leftNum = toNumeric(leftPrim);
  const rightNum = toNumeric(rightPrim);
  
  //좌우항의 타입이 같을 경우 그대로 각 타입의 비교 조건으로 비교
  if(typeof leftNum === typeof rightNum) {
    if(typeof leftNum === "bigint") return leftNum < rightNum;
    return numberIsLessThen(leftNum, rightNum);
  }
  
  //한쪽이 숫자형, 한쪽이 bigInt형일 때 비교
  if(Number.isNaN(leftNum) || Number.isNaN(rightNum)) return undefined;
  if(leftNum === Infinity || rightNum === -Infinity) return false;
  if(leftNum === -Infinity || rightNum === Infinity) return true;
  return leftNum < rightNum;
}

function isLessThen(left, right)
{
  return _isLessThen(left, right) ?? false;
}
  1. 좌항과 우항을 원시 자료형으로 변환한다. 이 때 preferType은 number로 한다.
  2. 좌항과 우항이 모두 문자열이면 문자열의 비교 연산을 수행한다.
    • 각각의 문자를 0부터 각 문자열의 길이의 최솟값까지 순회하여, 실질적으로 저장된 숫자값(자바스크립트는 utf-16으로 문자열을 인코딩한다)이 다르면 그것을 결과로 취한다.
    • 모든 문자열이 같을 경우, 길이를 비교한다.
  3. 한쪽이 BigInt형이고, 한쪽이 문자열이면 문자열을 BigInt로 형변환한 뒤 값을 비교한다.
    • 문자열을 BigInt형으로 변환하지 못했을 경우 최종 비교값은 undefined(=false가) 된다.
  4. 원시 자료형으로 변환된 각 항을 toNumeric 추상 연산을 적용하여 숫자형/BigInt형으로 변환한다.
  5. 만약 타입이 같을 경우, 그대로 비교한다.
    • 숫자형의 경우
      • 어느 한쪽이 NaN이면 undefined이다.
      • 어느 한쪽이 +0이고 다른 쪽이 -0이면 같은 것으로 취급한다.
      • 어느 한쪽이 양의 무한대이면 그 쪽을 큰 것으로 취급한다.
      • 어느 한쪽이 음의 무한대이면 그 쪽을 작은 것으로 취급한다.
      • 어느 쪽에도 해당하지 않을 경우 실수 비교 연산을 수행한다.
    • BigInt형의 경우
      • 실수 비교 연산을 수행한다.
  6. 어느 한쪽이 NaN이면 undefined이다.
  7. 어느 한쪽이 양의 무한대이면 그 쪽을 큰 것으로 취급하고, 음의 무한대이면 작은 것으로 취급한다.
  8. 각 항을 실수로 변경한 후 비교 연산을 수행한다.

비교 연산자(==)

동등 비교 연산자(==)는 추상 연산으로 isLooselyEqual이라는 연산을 이용하여 값을 계산한다. !=의 경우도 동일한 연산을 사용하며, 값만 반대일 뿐이다.

ECMAScript 명세서에서 정의하고 있는 ==의 형변환 과정은 다음과 같다. 이해를 돕기 위해 자바스크립트로 작성한 isLooselyEqual 코드를 같이 첨부한다.

function type(value)
{
  if(value === null) return "null";
  if(typeof value === "function") return "object";
  return typeof value;
}
function typeCheck(left, right, [type1, type2="any"])
{
  const leftType = type(left);
  const rightType = type(right);
  if(type2 === "any") return (leftType === type1 || rightType === type2);
  if(leftType === type1 && rightType === type2) return true;
  if(rightType === type1 && leftType === type2) return true;
  return false;
}
function objectTypeCheck(left, right)
{
  const rightValueType = ["number", "string", "symbol", "bigint"].includes(type(right));
  return type(left) === "object" && rightValueType;
}

function isLooselyEqual(left, right)
{
  //타입이 같을 시 strict 비교
  if(type(left) === type(right)) return left === right;
  
  //한쪽이 null이고 다른 쪽이 undefined이면 true
  if(typeCheck(left, right, ["null", "undefined"])) return true;
  
  //한쪽이 숫자형이고 다른 쪽이 문자열이면 문자열을 숫자로 변환
  if(typeCheck(left, right, ["number", "string"])) {
    return isLooselyEqual(+left, +right);
  }
  if(typeCheck(left, right, ["bigint", "string"])) {
    return isLooselyEqual(BigInt(left), BigInt(right));
  }
  
  //한쪽이 boolean형이면 불리언을 숫자로 변환
  if(typeCheck(left, right, ["boolean"])) {
    if(type(left) === "boolean") return isLooselyEqual(+left, right);
    if(type(right) === "boolean") return isLooselyEqual(left, +right);
  }
  
  //한쪽이 object형이면 object를 원시 자료형(perferType은 default)으로 변환
  if(objectTypeCheck(left, right)) return isLooselyEqual(toPrimitive(left), right);
  if(objectTypeCheck(right, left)) return isLooselyEqual(left, toPrimitive(right));
  
  //한쪽이 Number이고 다른 한쪽이 BigInt형일 때
  if(typeCheck(left, right, ["number", "bigint"])) {
    if(!Number.isFinite(left) || !Number.isFinite(right)) return false;
    return left == right;
  }
  return false;
}
  1. 좌항과 우항의 타입이 같으면 값을 기준으로 비교한다.
  2. 한쪽이 null이고 다른 쪽이 undefined이면 true를 반환한다.
  3. 한쪽이 숫자형이고 다른 쪽이 문자열이면 문자열을 숫자로 형변환한 후 비교한다.
  4. 한쪽이 BigInt형이고 다른 쪽이 문자열이면 문자열을 BigInt로 형변환한 후 비교한다.
  5. 한쪽이 Boolean형이면 불리언을 숫자로 형변환한다.
  6. 한쪽이 Object형이고 다른 한쪽이 숫자형(숫자로 형변환된 불리언 포함), 문자열, BigInt, Symbol형이면 Object를 추상 연산 toPrimitive()을 이용하여(preferType은 default이다) 원시 자료형으로 변경한 뒤, 이를 기반으로 계산한다.
  7. 한쪽이 숫자형이고 다른 쪽이 BigInt형이면
    • 한쪽이 NaN, Infinity, -Infinity면 false를 반환한다.
    • 각 항을 실수로 변경한 후 비교한 값을 결과로 취한다.
  8. 그 외의 경우, false를 반환한다.

맨 처음 문제의 답

답은 false다. new Date()Date.now()는 자료형이 각각 Object와 Number로 다르기 때문에, 형변환의 과정이 필요하다. 이 때, Object는 해당 오브젝트의 @@Symbol.toPrimitive 메소드를 우선적으로 호출하고(이 때 hint는 default이다) 없으면 valueOf, 그래도 없으면 toString 메소드를 호출한다. new Date()[Symbol.toPrimitive] 메소드가 존재하며, 그 값은 문자열이므로 문자열과 숫자를 비교하게 된다. 문자열과 숫자를 비교할 때에는 문자열 쪽이 숫자로 형변환되어서 비교된다. 이 때 NaN과 숫자는 다른 값이므로 false가 나온다.

profile
홍익인간이 되고 싶은 꿈꾸는 방랑자

0개의 댓글