추상 연산
명시적/암시적 강제변환의 세계로 떠나기 전에 어떻게 값이 문자열, 숫자, 불리언 등의 타입이 되는지, 그 기본 규칙을 알아보자. ES5를 보면 변환 규칙의 '추상 연산'이 정의되어 있다. ToString
, ToNumber
, ToBoolean
을 집중적으로 보고 ToPrimitive
는 대략만 훑어 보자.
ToString
'문자열이 아닌 값 -> 문자열' 변환 작업은 ES5 $9.8의 ToString
추상 연산 로직이 담당한다. 내장 원시 값은 본연의 무자열화 방법이 정해져 있다(ex) null -> "null"
). 숫자는 예상대로 그냥 문자열로 바뀌고 너무 작거나 큰 값은 지수 형태로 바뀐다.
일반 객체는 특별히 지정하지 않으면 기본적으로 toString()
메서드가 내부 [[class]]
를 반환한다.
자신의 toString()
메서드를 가진 객체는 문자열 처럼 사용하면 자동으로 이 메서드가 기본 호출되어 toString()
을 대체한다.
엄밀히는 '객체 -> 문자열'강제변환시 ToPrimitive 추상연산 과정을 거친다.
배열은 기본적으로 재정의된 toString()
이 있다. 문자열 변환시 모든 원소 값이 콤마로 분리된 형태로 이어진다.
var a = [1, 2, 3];
a.toString(); //("1, 2, 3");
또한 toString()
메서드는 명시적으로도 호출 가능하며, 문자열 콘텍스트에서 문자열이 아닌 값이 있을 경우에도 자동 호출된다.
JSON 문자열화
ToString
은 JSON.stringify()
유틸리티를 사용하여 어떤 값을 JSON 문자열로 직렬화하는 문제와도 연관된다.
JSON 문자열화는 강제변환과 똑같지는 않지만, 방금 전 살펴본 ToString
규칙과 관련 잇으므로 JSON 문자열 화에 대해 알아보자.
대부분 단순 값들은 직렬화 결과가 반드시 문자열이라는 점을 제외하고는, JSON 문자열화나 toString()
변환이나 기본적으로 같은 로직이다.
JSON.stringify(42); // "42"
JSON.stringify("42"); // ""42"" (따옴표가 붙은 문자열 인자를 문자열화 한다.)
JSON.stringify(null); // "null"
JSON.stringify(true); // "true"
JSON 안전값(JSON 표현형으로 확실히 나타낼 수 있는 값)은 모두 JSON.stringify()
로 문자열화 할 수 있다.
JSON 안전 값이 아닌 것들을 반대로 떠올리면 이해가 빠를 것이다. 이들은 모두 다른 언어로 인식하여 JSON 값으로 쓸 수 없는, 표준 JSON 규격을 벗어난 값이다.
JSON.stringify()
는 인자가 undefined
, 함수, 심벌 값이면 자동으로 누락시키며 이런 값들이 만약 배열에 포함되어 있으면 null
로 바꾼다. 객체 프로퍼티에 있으면 간단히 지워버린다.
혹시라도 JSON.stringify()
에 환형 참조 객체를 넘기면 에러가난다. 객체 자체에 toJSON()
메서드가 정의되어 있다면, 먼저 이 메서드를 호출하여 직렬화한 값을 반환한다.
부적절한 JSON 값이나 직렬화하기 곤란한 객체 값을 문자열화 하려면 toJSON()
메서드를 따로 정의해야 한다. 예를 들면,
var o = {};
var a = {
b: 42,
c: 0,
d: function () {},
};
// 'a'를 환형 참조 객체로 만든다.
o.e = a;
// 환형 참조 객체는 JSON 문자열화 시 에러가 난다.
// JSON.stringify( a );
// JSON 값으로 직렬화하는 함수를 따로 정의한다.
a.toJSON = function () {
// 직렬화에 프로퍼티 'b'만 포함시킨다.
return { b: this.b };
};
JSON.stringify(a); // "{b : 42}"
toJSON()
이 JSON 문자열 표현형을 반환하리라 넘겨짚는건 아주 흔한 오해다. 문자열을 문자열화할 의도가 아니라면 정확하지 않을 가능성이 높다. toJSON()
은 적절히 평범한 실제 값을 반환하고 문자열화 처리는 JSON.stringify()
이 담당한다.
다시말해 toJSON()
의 역할은 '문자열화하기 적당한 JSON 안전 값으로 바꾸는 것'이지, 'JSON 문자열로 바꾸는 것'이 아니다. 많은 개발자가 잘못 알고 있는 부분이다.
var a = {
val: [1, 2, 3],
// 맞다!
toJSON: function () {
return this.val.slice(1);
},
};
var b = {
val: [1, 2, 3],
// 틀렸다!
toJSON: function () {
return "[" + this.val.slice(1).join() + "]";
},
};
JSON.stringify(a); // "[2, 3]"
JSON.stringify(b); // ""[2, 3]""
두 번째 호출 코드는 배열 자체가 아니라 반환된 문자열을 다시 문자열화 한다. 이건 개발자가 의도했던 바가 아닐 것이다.
JSON.stringify()
를 얘기하다 보니, 잘 알려지지 않은 유용한 기능 하나를 보자. 배열 아니면 함수 형태의 대체자를 JSON.stringify()
의 두 번째 선택 인자로 지정하여 객체를 재귀적으로 직렬화 하면서 필터링 하는 방법이 있다. toJSON()
이 직렬화할 값을 준비하는 방식과 비슷하다.
대체자가 배열이면 전체 원소는 문자열이어야 하고 각 원소는 객체 직렬화의 대상 프로퍼티명이다. 즉, 여기에 포함되지 않은 프로퍼티는 직렬화 과정에서 빠진다.
대체자가 함수면 처음 한 번은 객체 자신에 대해, 그다음엔 각 객체 프로퍼티별로 한 번씩 실행하면서 매번 키와 값 두 인자를 전달한다. 직려로하 과정에서 해당 키를 건너뛰려면 undefiend
를, 그외엔 해당 값을 반환한다.
var a = {
b: 42,
c: "42",
d: [1, 2, 3],
};
JSON.stringify(a, ["b", "c"]); // "{"b" : 42, "c" : "42"}"
JSON.stringify(a, function (k, v) {
if (k !== "c") return v;
});
// "{"b" : 42, "d" : "[1, 2, 3]"}"
함수인 대체자는 최초 호축시 키 인자 k는 undefined다. 대체자는 if 문에서 키가 "c"인 프로퍼티를 솎아낸다. 문자열화는 재귀적으로 이루어지므로 배열 [1, 2, 3]의 각 원소는 v로, 인덱스는 k로 각각 대체자 함수에 전달된다.
JSON.stringify()
은 세 번째 선택 인자는 스페이스라고 하며 사람이 읽기 쉽도록 들여쓰기를 할 수 있다. 들여 쓰기를 할 빈 공간의 개수를 숫자로 지정하거나 문자열을 지정하여 각 들여 쓰기 수준에 사용한다.
var a = {
b: 42,
c: "42",
d: [1, 2, 3],
};
JSON.stringify(a, null, 3);
// "{
// "b": 42,
// "c": "42",
// "d": [
// 1,
// 2,
// 3
// ]
// }"
JSON.stringify()
은 직접적인 강제변환의 형식은 아니지만 두 가지 이유로 ToString
강제 변환과 연관된다.
- 문자열, 숫자, 불리언, null 값이 JSON으로 문자열화 하는 방식은
ToString
추상 연산의 규칙에 따라 문자열 값으로 강제변환 되는 방식과 동일하다. JSON.stringify()
에 전달한 객체가 자체toJSON()
메서드를 가지고 있다면, 문자열화 전toJSON()
가 자동 호출되어 JSON 안전 값으로 '강제변환'된다.
참고
- You Don't Know JS ( 한빛 미디어 )
'JAVASCRIPT' 카테고리의 다른 글
[JS][강제변환] 추상 연산 ToBoolean (0) | 2022.02.09 |
---|---|
[JS][강제변환] 추상연산 ToNumber (0) | 2022.02.09 |
[JS][네이티브][마무리] 네이티브의 프로토타입 (0) | 2022.02.08 |
[JS][네이티브] Symbol() (0) | 2022.02.08 |
[JS][네이티브] Date() and Error() (0) | 2022.02.08 |
댓글