글 작성자: 택시 운전사
반응형

해당 글은 JavaScript values: not everything is an object를 번역한 글입니다.

질문

{} + {}의 결과는 뭘까? 자바스크립트에서 객체나 배열들을 더할 때 우리는 종종 예상치 못한 결과물을 얻곤한다. 이러한 결과물이 생긴 이유에 대해서 알아보자!

자바스크립트의 더하기 연산 법칙은 간단한다.

오직 숫자(number)나 문자열(string)만 더할 수 있다.

숫자와 문자열 이외의 다른 모든 값들은 더하기 연산을 만나면 이 둘 중 하나로 변하게 된다. 해당 변환이 어떻게 이루어지는 지 알기 위해서는, 몇 가지 사실에 대해서 이해해야한다.

자바스크립트는 원시값(primitives)과 객체(objects) 두 종류의 값으로 이루어져 있다. 원시값에는 undefined, null, boolean, numbers, strings가 있다. 그리고 배열(arrays)과 함수(functions)를 포함한 모든 다른 값들은 objects이다.

값 변환하기

더하기 연산자(+)는 세 종류의 변환을 하는데, 원시값을 숫자나 문자열로 바꿔준다.

ToPrimitive()를 이용하여 값을 원시값으로 바꾸기

ToPrimitive()는 다음과 같이 정의된다.

ToPrimitive(input, PreferredType?)

추가적 인자인 PreferredTypeNumber 혹은 String이며 선호하는 타입을 알려주는 역할을 한다. 해당 함수의 결과물은 항상 어떠한 원시값이다. 만약 PreferredTypeNumber이면 다음과 같은 변환 과정이 일어난다.

  1. 만약 입력값이 원시값이면, 그대로 출력한다.
  2. 그렇지 않고, 입력값이 객체이면, obj.valueOf()를 호출한다. 해당 함수의 결과값이 원시값이라면, 이를 반환한다.
  3. 그렇지 않으면, obj.toString()을 호출한다. 만약 결과값이 원시값이라면, 이를 반환한다.
  4. 그렇지 않으면, TypeError를 발생시킨다.

만약 PreferredTypeString이라면, 2, 3번 단계는 서로 바뀌게된다. 만약 PreferredType이 없다면 DateNumber대신 String으로 설정된다.

ToNumber()를 이용하여 값을 숫자로 바꾸기

아래 표는 ToNumber()에 의해 원시값이 어떻게 숫자로 바뀌는 지 보여주는 표이다.

인자 결과물
undefined NaN
null +0
boolean true1로 변환되고, false+0으로 변환된다.
number 변환이 필요없다.
string 숫자를 문자열로 파싱한다. 예를들어 "324"324로 변환된다

객체 objToPrimitive(obj,Number)를 이용하여 숫자로 변환된다. 그리고 결과물에 ToNumber()를 적용한다.

ToString()을 이용하여 값을 문자열로 바꾸기

아래 표는 ToString()에 의해 원시값이 어떻게 문자열로 바뀌는 지 보여주는 표이다.

인자 결과물
undefined "undefined"
null "null"
boolean true"true"로 변환되고, false"false"으로 변환된다.
number 숫자를 문자열로 표시한 값. 예를 들어 324"324"로 변환된다.
string 변환이 필요없다.

객체 objToPrimitive(obj,String)을 이용하여 숫자로 변환된다. 그리고 결과물에 ToString()을 적용한다.

예시

let obj = {
    valueOf: function () {
        console.log("valueOf");
        return {}; // not a primitive
    },
      toString: function () {
          console.log("toString");
          return {}; // not a primitive
    }
}

Number는 내부적으로 ToNumber()를 호출하는 함수이다.

> Number(obj)
valueOf
toString
TypeError: Cannot convert object to primitive value

toString()의 결과가 원시값이 아니기 때문에 Number에서 TypeError가 발생한다.

더하기 연산

다음 표현식이 주어졌다고 생각해보자.

value1 + value2

다음 표현식을 계산하기 위해서는 다음과 같은 과정을 거쳐야한다.

  1. 양쪽 연산대상을 원시값으로 변환한다.

     prim1 := ToPrimitive(value1)
     prim2 := ToPrimitive(value2)
  2. 만약 prim1이나 prim2가 문자열이라면, 두 연산대상 모두를 문자열로 바꾸고 둘을 이은(concatenation) 값을 결과로 내놓는다.

  3. 그렇지 않으면, 둘을 숫자로 바꿔서 둘을 더한 값을 결과로 내놓는다.

결과 예상해보기

두 비어있는 배열을 더할 때, 모두 예상하는 것처럼 동작한다.

> [] + []
''

우선 []를 원시값으로 바꾸는 과정은 valueOf()를 사용한다. 이 경우 배열은 배열 그 자체를 내놓게 된다.

> let arr = [];
> arr.valueOf() === arr
true

결과가 원시값이 아니기 때문에 toString()이 호출될 것이고, 원시값이 아닌 빈 문자열을 결과로 내놓는다. 따라서, [] + []의 결과는 두 빈 문자열을 이은 값인 ""가 된다. 배열과 객체를 더하는 것도 우리가 예상하는 것처럼 작동한다.

> [] + {}
'[object Object]'

빈 객체를 문자열로 바꾸면 다음과 같이 된다.

> String({})
'[object Object]'

이전의 결과인 """[object Object]"를 이어서 최종 결과인 "[object Object]"가 된다.

이외에도 객체를 원시값으로 바꾸는 다양한 예제들이 있다.

> 5 + new Number(7)
12
> 6 + { valueOf: function () { return 2 } }
8
> "abc" + { toString: function () { return "def" } }
'abcdef'

예상치 못한 결과들

하지만 만약 더하기 연산자의 첫번째 연상대상이 비어있는 객체({})라면 상황이 좀 달라진다.

> {} + {}
NaN

무슨 일이 일어난걸까? 문제는 자바스크립트가 첫번째 {}를 비어있는 코드블록으로 해석하고 무시한 것이다. 따라서 NaN+{}로 계산된 결과이다. 여기서 보이는 +는 이진법 더하기 연산자가 아니라, 연산대상을 숫자로 바꿔주는 접두사이다. 이는 Number()와 비슷한 역할이다.

> +"3.65"
3.65

다음 표현식들은 모두 같은 의미이다.

+{}
Number({})
Number({}.toString()) // {}.valueOf()는 원시값이 아니다.
Number("[object Object]")
NaN

왜 첫번째 {}는 코드 블록으로 해석되었을까? 왜냐하면 선언문 맨 앞에 있는 중괄호쌍은 코드 블록으로 해석되기 때문이다. 따라서 다음과 입력값을 다음과 같이 바꾸면 원하는 결과를 얻는다.

> ({} + {})
'[object Object][object Object]'

함수나 메소드의 인자는 항상 표현식으로 파싱된다.

> console.log({} + {})
'[object Object][object Object]'

이제 우리는 다음과 같은 결과도 해석할 수 있을 것이다.

> {} + []
0

다시 말하지만, {}가 코드 블록으로 해석되어, +[]만 남게된다. 다음 표현식들도 모두 같은 의미이다.

+[]
Number([])
Number([].toString()) // [].valueOf()는 원시값이 아니다.
Number("")
0

흥미로운 사실은, Node.js는 다른 방식으로 파싱한다. Node.js는 다음의 결과물들은 예상되는 결과를 보여준다.

> {} + {}
'[object Object][object Object]'
> {} + []
'[object Object]'

Node.jsconsole.log()를 사용했을 때처럼 더욱 예상되는 결과를 얻는다. 하지만, 입력으로 선언문을 사용하지 않는 경향을 보인다.

그럼 배열이나 객체는 어떻게 합칠까?

대부분의 환경에서, 자바스크립트에서 +가 어떻게 작동하는 지 이해하는 것은 어렵지 않다. 당신은 그저 숫자나 문자열을 더하기만 하면 될 뿐이다. 객체들은 문자열이나 숫자로 변환된다. 만약 당신이 배열을 연결하고 싶다면 다음과 같은 메소드를 사용하면 된다.

> [1, 2].concat([3, 4])
[1, 2, 3, 4]

자바스크립트에서 빌트인으로 객체를 합치는 방법은 없다. 대신 Lodash같은 라이브러리를 이용해야한다. ES6Destructing을 이용해도 같은 결과를 얻을 수 있다.

> var obj1 = { kim: 1, han: 2 };
> var obj2 = { suji: 3, minsu: 4 };
> _.extend(obj1, obj2)
{ kim: 1, han: 2, suji: 3, minsu: 4 }
>{ ...obj1, ...obj2 }
{ kim: 1, han: 2, suji: 3, minsu: 4}

참고 자료

반응형