심볼
자바스크립트는 객체의 프로퍼티 키로 문자열과 심볼형만 가능하다고 했다. 그럼 심볼형이란 무엇일까?
'심볼(symbol)'은 유일한 식별자(unique indentifier)를 만들고 싶을 때 사용한다.
Symbol()을 사용해서 심볼값을 만들 수 있다. 매개변수를 넣으면 심볼 이름을 붙일 수 있다.
let id1 = Symbol();
let id2 = Symbol("id2")
심볼은 유일성이 보장되는 자료형이기 때문에 설명이 같은 심볼을 만들어도 각 심볼값은 다르다.
심볼 이름은 단지 심볼에 붙이는 설명이 될 뿐 심볼값에 영향을 주지 않는다.
자바스크립트의 대부분의 자료형은 문자형으로의 암시적 형 변환이 일어나지만 심볼형은 그렇지 않다.
심볼을 출력해야하는 상황이라면 toString()을 명시적으로 호출해야 한다.
또는 description 프로퍼티를 이용해서 심볼 이름만 보여주는 것도 가능하다.
let id = Symbol("id");
alert(id) // TypeError: Cannot convert a Symbol value to a string
alert(id.toString()); // Symbol(id)가 얼럿 창에 출력됨
lert(id.description); // id
hidden 프로퍼티
심볼을 이용하면 hidden 프로퍼티를 만들 수 있다.
hidden 프로퍼티는 외부 코드에서의 접근과 값을 덮어쓰는 것이 불가능한 프로퍼티이다.
// 서드파티 코드에서 가져온 객체
let user = {
name: "John"
};
let id = Symbol("id");
user[id] = 1;
alert( user[id] ); // 심볼을 키로 사용해 데이터에 접근
user에 식별자를 붙여주기 위해 Symbol("id")를 프로퍼티 키값으로 설정했다.
user는 서드파티 코드에서 가져온 객체이기 때문에 함부로 새로운 프로퍼티를 추가할 수 없다.
그러나 심볼은 서드파티 코드에서 접근할 수 없기 때문에 심볼을 사용해서 서드파티 코드가 모르게 user에 식별자를 부여한 것이다.
// ...
let id = Symbol("id");
user[id] = "외부 스크립트 id 값";
또 다른 상황으로 자바스크립트 라이브러리와 같은 외부 스크립트에서 서드파티 코드 객체인 user를 식별해야 한다고 해보자.
외부 스크립트는 위와 같이 전용 식별자를 만들어서 사용할 수 있을 것이다.
만약에 심볼을 사용하지 않고 문자열 "id"를 사용해서 식별자를 만들었다면 충돌이 발생할 가능성이 있다.
객체 리터럴을 사용해서 객체를 만들고 심볼을 프로퍼티 키로 설정하고 싶다면 대괄호를 사용하면 된다.
let id = Symbol("id");
let user = {
name: "John",
[id]: 123 // "id": 123은 안됨
};
또한 키가 심볼인 프로퍼티는 for...in 구문에서 제외된다.
let id = Symbol("id");
let user = {
name: "John",
age: 30,
[id]: 123
};
for (let key in user) alert(key); // name과 age만 출력되고, 심볼은 출력되지 않음
Object.keys(user)에서도 키가 심볼인 프로퍼티는 배제되지만 Object.assign은 키가 심볼인 프로퍼티를 배제하지 않고 객체 내 모든 프로퍼티를 복사한다.
내장 메서드 Object.getOwnPropertySymbols(obj)를 사용하면 모든 심볼을 볼 수 있고, Reflect.ownKeys(obj) 메서드를 호출하면 심볼형 키를 포함한 객체의 모든 키를 반환한다.
심볼을 완전히 숨길 수는 없지만 대부분의 라이브러리, 내장 함수 등은 위 메서드를 사용하지 않는다.
전역 심볼
심볼 이름이 같아도 심볼 값은 다르다고 했다.
그러나 여러 곳에서 심볼 이름이 같은 객체에 접근해야 할 수도 있는데 전역 심볼 레지스트리는 이런 경우를 위해 만들어졌다.
전역 심볼 레지스트리 안에 심볼을 만들고 해당 심볼에 접근하면 이름이 같은 경우 항상 동일한 심볼을 반환한다.
레지스트리 안에 있는 심볼을 읽거나 새로운 심볼을 생성하려면 Symbol.for(key)를 사용하면 된다.
// 전역 레지스트리에서 심볼을 읽고 없으면 새로 만듦
let id = Symbol.for("id");
let id2 = Symbol.for("id"); // 같은 심볼 반환
alert(id === id2); // true
Symbol.for(key)는 이름을 가진 심볼을 반환하고 Symbol.keyFor(sym)은 해당 심볼의 이름을 반환한다.
// 이름을 이용해 심볼을 찾음
let sym = Symbol.for("name");
let sym2 = Symbol.for("id");
// 심볼을 이용해 이름을 얻음
alert( Symbol.keyFor(sym) ); // name
alert( Symbol.keyFor(sym2) ); // id
전역 심볼에 대해서만 사용할 수 있고 전역 심볼이 아닌 인자가 넘어오면 Symbol.keyFor는 undefined를 반환한다.
일반 심볼 이름을 알고 싶다면 description 프로퍼티에 접근하면 되고 전역 심볼 이름을 알고 싶다면 Symbol.keyFor를 사용하면 된다.
ToPrimitive
객체 형 변환은 세 종류로 구분되는데 'hint'라고 불리는 값이 구분 기준이 된다.
hint는 목표로 하는 자료형 정도로 생각하면 된다.
1. string
alert와 같이 문자열을 기대하는 연산을 수행할 때 hint는 string이 되고 객체는 문자형으로 형 변환이 된다.
// 객체를 출력하려고 함
alert(obj);
// 객체를 프로퍼티 키로 사용하고 있음
anotherObj[obj] = 123;
2. number
수학 연산을 적용하려고 할 때 hint는 number가 되고 객체는 숫자형으로 형 변환이 된다.
// 명시적 형 변환
let num = Number(obj);
// (이항 덧셈 연산을 제외한) 수학 연산
let n = +obj; // 단항 덧셈 연산
let delta = date1 - date2;
// 크고 작음 비교하기
let greater = user1 > user2;
3. default
연산자가 기대하는 자료형이 확실하지 않을 때 hint는 default가 된다. 아주 드물게 발생한다.
이항 덧셈 연산자 +는 문자열을 합치는 연산과 숫자를 더해주는 연산 둘 다 가능하기 때문에 이 +의 피연산자가 객체이면 hint는 default가 된다.
동등 연산자 ==를 사용해서 객체-문자형, 객체-숫자형, 객체-심볼형끼리 비교할 때도 hint는 default가 된다.
// 이항 덧셈 연산은 hint로 `default`를 사용
let total = obj1 + obj2;
// obj == number 연산은 hint로 `default`를 사용
if (user == 1) { ... };
비교 연산자 <, >도 피연산자가 문자형, 숫자형이 될 수 있지만 이 경우는 hint를 number로 고정한다.
또한 모든 객체는 true이기 때문에 boolean hint는 없다.
자바스크립트는 형 변환이 필요할 때 아래와 같은 알고리즘에 따라 원하는 메서드를 찾고 호출한다.
- 객체에 obj[Symbol.toPrimitive](hint) 메서드가 있는지 찾고 있다면 호출한다. Symbol.toPrimitive는 시스템 심볼로 심볼형 키로 사용된다.
- 1에 해당하지 않고 hint가 string이라면, obj.toString()이나 obj.valueOf()를 호출한다.
- 1, 2에 해당하지 않고 hint가 number 또는 default라면, obj.valueOf()나 obj.toString()을 호출한다.
Symbol.toPrimitive
Symbol.toPrimitive는 자바스크립트의 내장 심볼로 목표로 하는 자료형(hint)를 명명하는 데 사용된다.
user 객체에 객체-원시형 변환 메서드 obj[Symbol.toPrimitive](hint)를 구현해보자.
let user = {
name: "John",
money: 1000,
[Symbol.toPrimitive](hint) {
alert(`hint: ${hint}`);
return hint == "string" ? `{name: "${this.name}"}` : this.money;
}
};
alert(user); // hint: string -> {name: "John"}
alert(+user); // hint: number -> 1000
alert(user + 500); // hint: default -> 1500
이렇게 메서드를 구현함으로써 user는 hint에 따라 문자열로 변환되기도 하고 숫자로 변환되기도 한다.
메서드 하나로 모든 종류의 형 변환을 다룰 수 있다.
toString과 valueOf
객체 형 변환을 할 때 Symbol.toPrimitive가 없으면 toString이나 valueOf를 호출한다고 했다.
- hint == "string" : toString -> valueOf 순으로 호출
- 그 외 : valueOf -> toString 순으로 호출
이 메서드들은 반드시 원시값을 반환해야 하고 객체를 반환하면 그 결과는 무시된다.
일반 객체는 기본적으로 다음 규칙을 따른다.
- toString은 문자열 "[object Object]"를 반환
- valueOf는 객체 자신을 반환
valueOf는 객체를 반환하기 때문에 그 결과가 무시된다.
이제 이 메서드들을 사용해서 Symbol.toPrimitive를 호출한 것과 같은 결과를 내도록 만들어보자.
let user = {
name: "John",
money: 1000,
// hint가 "string"인 경우
toString() {
return `{name: "${this.name}"}`;
},
// hint가 "number"나 "default"인 경우
valueOf() {
return this.money;
}
};
alert(user); // toString -> {name: "John"}
alert(+user); // valueOf -> 1000
alert(user + 500); // valueOf -> 1500
형 변환을 한 곳에서 처리해주고 싶으면 toString만 구현하면 된다.
객체에 Symbol.toPrimitive와 valueOf가 없으면 toString이 모든 형 변환을 처리한다.
let user = {
name: "John",
toString() {
return this.name;
}
};
alert(user); // toString -> John
alert(user + 500); // toString -> John500