ECMAScript 6 Symbol과 Symbol 프로퍼티

Symbols과 Symbol 프로퍼티

Symbol은 ECMAScript 6에서 도입된 Primitive 타입으로, 기존의 string, number, boolean, null, undefined와 같은 타입입니다. Symbol은 객체의 Private 멤버를 생성하는 방법으로 시작되었는데, JavaScript 개발자들이 오랫동안 원했던 기능입니다. Symbol 이전에는 이름이 있는 프로퍼티는 이름의 모호함에 관계없이 쉽게 액세스할 수 있었고 Private name 기능은 개발자가 문자열이 아닌 프로퍼티 이름을 만들 수 있도록 하기위한 것입니다. 그리고 일반적인 방법으로 Private name에 대한 탐지는 작동하지 않습니다.

Private name에 대한 제안은 마침내 ECMAScript 6 Symbol로 진화했습니다. 이장에서는 Symbol을 효과적으로 사용하는 법을 가르쳐줄 것입니다. 구현 세부 사항은 동일하게 유지되었지만 (즉, 프로퍼티 이름에 문자열이 아닌 값을 추가 한 경우) Privacy에 대한 부분은 삭제되었습니다. 대신 Symbol 프로퍼티는 다른 객체 프로퍼티와 구분되어 분류됩니다.

Symbol 생성하기

Symbol은 JavaScript Primitive 중, boolean은 true, number는 42와 같은 리터럴이 없는 유일한 타입니다. 아래 예제와 같이 전역Symbol 함수를 사용하여 Symbol을 만들 수 있습니다.

1
2
3
4
5
let firstName = Symbol();
let person = {};
person[firstName] = "Nicholas";
console.log(person[firstName]); // "Nicholas"

위 예제의 Symbol firstNameperson 객체에 새로운 프로퍼티를 할당하기 위해 만들어지고 사용됩니다. 이 Symbol은 동일한 프로퍼티에 액세스할 때마다 사용 해야합니다. Symbol 변수에 적절한 이름을 부여하는 것은 좋은 생각입니다. Symbol이 무엇을 나타내는지 쉽게 알 수 있기 때문입니다.

Symbol은 Primitive이기 때문에 new Symbol()을 호출하면 오류가 발생합니다. Symbol의 인스턴스를 new Object (yourSymbol)을 통해서 만들 수도 있습니다. 하지만 이 기능이 그렇게 유용하지 않습니다.

Symbol 함수는 Symbol에 대한 설명을 위해 Optional 파라미터도 받아들입니다. 설명 자체는 프로퍼티에 액세스하는데 사용할 수 없지만 디버깅 목적으로 사용될 수 있습니다.

1
2
3
4
5
6
7
8
let firstName = Symbol("first name");
let person = {};
person[firstName] = "Nicholas";
console.log("first name" in person); // false
console.log(person[firstName]); // "Nicholas"
console.log(firstName); // "Symbol(first name)"

Symbol의 설명은 내부적으로 [[Description]] 프로퍼티에 저장됩니다. 이 프로퍼티는 SymboltoString() 메서드가 명시적으로 또는 암시적으로 호출될 때마다 읽혀집니다. firstName SymboltoString() 메서드는 이 예제에서 console.log()에 의해 암시적으로 호출되므로 설명이 log에 출력됩니다. 코드에서 직접 [[Description]]에 액세스할 수 없습니다. 필자는 항상 Symbol을 읽고 디버깅하기 쉽도록 설명을 제공할 것을 권장합니다.

Symbol 식별하기

Symbol은 Primitive이기 때문에 typeof 연산자를 사용하여 변수에 Symbol이 포함되어 있는지 확인할 수 있습니다. ECMAScript 6은 typeof를 확장하여 Symbol에 사용될 때 "symbol"을 반환하도록 합니다.

1
2
let symbol = Symbol("test symbol");
console.log(typeof symbol); // "symbol"

변수가 Symbol인지 여부를 결정하는 다른 간접적인 방법이 있지만 typeof 연산자가 가장 정확하고 선호되는 기술입니다.

Symbol 사용하기

계산된 프로퍼티 이름을 사용하는 곳이면 어디에서나 Symbol을 사용할 수 있습니다. 이장에서 Symbol 과 함께 사용된 괄호 표기법을 이미 보았지만 아래의 호출과 같이 Object.defineProperty()Object.defineProperties()뿐만 아니라 계산된 객체 리터럴 프로퍼티 이름에서도 Symbol을 사용할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let firstName = Symbol("first name");
// 계산된 객체 리터럴 프로퍼티 사용
let person = {
[firstName]: "Nicholas"
};
// 프로퍼티를 읽기전용으로 만듭니다.
Object.defineProperty(person, firstName, { writable: false });
let lastName = Symbol("last name");
Object.defineProperties(person, {
[lastName]: {
value: "Zakas",
writable: false
}
});
console.log(person[firstName]); // "Nicholas"
console.log(person[lastName]); // "Zakas"

위 예제는 먼저 계산된 객체 리터럴 프로퍼티를 사용하여 firstName Symbol 프로퍼티를 만듭니다. 그 다음 라인은 프로퍼티를 읽기 전용으로 설정합니다. 그후에, Object.defineProperties() 메서드를 사용하여 읽기 전용 lastName Symbol 프로퍼티를 생성합니다. 계산된 객체 리터럴 프로퍼티가 다시 한번 사용되지만 Object.defineProperties() 호출의 두 번째 파라미터의 일부입니다.

Symbol은 계산된 프로퍼티 이름이 허용되는 곳이면 어디에서나 사용할 수 있지만, 효과적으로 사용할 수 있도록 이들 Symbol*을 다른 코드 사이에서 공유할 수있는 시스템이 필요합니다.

Symbol 공유하기

여러분은 코드의 다른 부분에서 같은 Symbol을 사용하기 원할 수도 있습니다. 예를 들어, 응용 프로그램에서 고유 식별자를 나타내기 위해 동일한 Symbol 프로퍼티를 사용해야하는 두개의 다른 객체 타입이 있다고 가정하겠습니다. 파일이나 큰 코드베이스에서 Symbol을 추적하는 것은 어렵고 오류가 발생할 수 있습니다. 그래서 ECMAScript 6는 어느 시점에서나 액세스할 수 있는 전역 Symbol 레지스트리를 제공합니다.

공유할 Symbol을 생성하려면 Symbol() 메서드를 호출하는 대신 Symbol.for() 메서드를 사용합니다. Symbol.for() 메서드는 여러분이 생성하고자하는 Symbol을 위한 문자열 식별자로 단일 파라미터를 받아들입니다. 그리고 이 파라미터는 Symbol의 설명으로도 사용됩니다.

1
2
3
4
5
6
7
let uid = Symbol.for("uid");
let object = {};
object[uid] = "12345";
console.log(object[uid]); // "12345"
console.log(uid); // "Symbol(uid)"

Symbol.for() 메서드는 먼저 전역 Symbol 레지스트리를 검색하여 “uid” 키가 있는 Symbol이 있는지 확인합니다. 만약 Symbol이 있다면 이 메서드는 기존의 Symbol을 리턴합니다. 그런데 만약 없다면 새로운 Symbol을 생성하고 지정된 키를 사용하여 전역 Symbol 레지스트리에 등록합니다. 그리고 새로운 Symbol을 리턴합니다. 즉, 같은 키를 사용하는 Symbol.for()에 대한 후속 호출은 다음과 같이 동일한 Symbol을 반환합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
let uid = Symbol.for("uid");
let object = {
[uid]: "12345"
};
console.log(object[uid]); // "12345"
console.log(uid); // "Symbol(uid)"
let uid2 = Symbol.for("uid");
console.log(uid === uid2); // true
console.log(object[uid2]); // "12345"
console.log(uid2); // "Symbol(uid)"

위 예제에서 uiduid2는 같은 Symbol을 사용하고 있습니다. 그래서 서로 바꿔서 사용할 수도 있습니다. Symbol.for()에 대한 첫 번째 호출은 Symbol을 생성하고 두 번째 호출은 전역 Symbol 저장소에서 Symbol을 가져옵니다.

공유 Symbol의 또 다른 독특한 부분은 Symbol.keyFor() 메서드를 호출하여 전역 Symbol 레지스트리에서 Symbol과 연관된 키를 검색할 수 있다는 것입니다.

1
2
3
4
5
6
7
8
let uid = Symbol.for("uid");
console.log(Symbol.keyFor(uid)); // "uid"
let uid2 = Symbol.for("uid");
console.log(Symbol.keyFor(uid2)); // "uid"
let uid3 = Symbol("uid");
console.log(Symbol.keyFor(uid3)); // undefined

uiduid2는 모두 “uid”키를 반환합니다. Symbol uid3은 전역 Symbol 레지스트리에 존재하지 않으므로, 관련된 키가 없으며Symbol.keyFor()undefined를 리턴합니다.

글로벌 Symbol 레지스트리는 글로벌 Scope와 같은 공유 환경입니다. 이는 해당 환경에 이미 존재하거나 존재하지 않는 것에 대해 가정할 수 없음을 의미합니다. 서드파티 컴포넌트를 사용할 때 충돌을 일으킬 가능성을 줄이기 위해 Symbol 키의 네임 스페이스를 사용할 수 있습니다. 예를 들어,jQuery 코드는 "jquery.element"또는 유사한 방법으로 모든 키에 "jquery." 접두사를 사용할 수 있습니다.

Symbol 강제 변환 (Coercion)

타입 강제 변환은 JavaScript의 중요한 부분이며, 한 데이터 타입을 다른 데이터 타입으로 강제 변환하는 것은 많은 유연성이 있습니다. 그러나 Symbol은 타입 강제 변환에 있어서는 꽤 융통성이 없습니다. 왜냐하면 다른 타입은 Symbol과 논리적으로 동등하지 않기 때문입니다. 특히 Symbolstring이나 number로 강제 변환 될 수 없으므로 실수로 Symbol로 예상되는 프로퍼티에 다른 타입을 사용할 수 없습니다.

이 장에서는 console.log()를 사용하여 Symbol에 대한 결과를 보여줍니다. console.log()Symbol에 대해 String()을 호출하여 유용한 출력을 생성합니다. String()을 직접 사용해도 같은 결과를 얻을 수 있습니다.

1
2
3
4
let uid = Symbol.for("uid"),
desc = String(uid);
console.log(desc); // "Symbol(uid)"

String() 함수는 uid.toString()을 호출고 Symbol은 설명 문자열을 반환합니다. 그러나 Symbol을 문자열과 직접 연결하려고 하면 오류가 발생합니다.

1
2
let uid = Symbol.for("uid"),
desc = uid + ""; // error!

uid와 빈 문자열을 연결하려면 uid가 먼저 string으로 강제 변환 되어야합니다. JavaScript는 Symbol의 강제 변환이 발견되면 에러를 발생시킵니다. 비슷하게, Symbolnumber로 강제 변환할 수도 없습니다. 모든 수학 연산자가 Symbol에 적용될 때 오류를 발생시킵니다.

1
2
let uid = Symbol.for("uid"),
sum = uid / 1; // error!

이 예제는 Symbol 변수를 1로 나눕니다. 사용된 수학 연산자에 관계없이 모두 오류가 발생합니다. 하지만 논리 연산자는 JavaScript의 다른 비어 있지 않은 값과 마찬가지로 true와 동일한 것으로 간주되기 때문에 오류가 발생하지 않습니다.

Symbol 프로퍼티 검색하기

Object.keys()Object.getOwnPropertyNames() 메서드는 객체의 모든 프로퍼티 이름을 검색할 수 있습니다. 전자의 경우 열거 가능한 모든 프로퍼티 이름을 반환하고 후자는 열거 가능 여부에 관계없이 모든 프로퍼티를 반환합니다. 그러나 두 메서드 모두 ECMAScript 5 기능을 유지하기 위해 Symbol 프로퍼티를 반환하지 않습니다. 대신, 객체로부터 Symbol 프로퍼티를 검색할 수 있도록하기 위해 Object.getOwnPropertySymbols() 메서드가 ECMAScript 6에 추가되었습니다.

Object.getOwnPropertySymbols()의 리턴 값은 자신의 Symbol 프로퍼티의 Array입니다.

1
2
3
4
5
6
7
8
9
10
let uid = Symbol.for("uid");
let object = {
[uid]: "12345"
};
let symbols = Object.getOwnPropertySymbols(object);
console.log(symbols.length); // 1
console.log(symbols[0]); // "Symbol(uid)"
console.log(object[symbols[0]]); // "12345"

이 코드에서 objectuid라는 단일 Symbol 프로퍼티를 가지고 있습니다. Object.getOwnPropertySymbols()에서 반환된 ArraySymbol을 포함하는 Array입니다.

모든 객체는 0개의 자체 Symbol 프로퍼티로 시작하지만 프로토타입에서 Symbol 프로퍼티를 상속받을 수 있습니다. ECMAScript 6는 Well-known Symbol이라고 불리는 미리 구현된 여러 프로퍼티를 정의합니다.

Well-Known Symbol을 이용한 내부 Operation 표현

ECMAScript 5의 핵심 테마는 JavaScript의 “magic” 부분 중 일부를 노출하고 정의하는 것이 었습니다. 이부분은 개발자가 Emulate할 수 없는 부분이었습니다. ECMAScript 6는 이전 버전 언어의 내부 논리를 더 많이 드러냄으로써 그 전통을 이어 나갔습니다. 주로 특정 객체의 기본 동작을 정의하기 위해 Symbol 프로토 타입 프로퍼티를 사용합니다.

ECMAScript 6에는 이전에 내부 전용 작업으로 간주되었던 JavaScript의 일반적인 동작을 나타내는 Well-known Symbol이라는 미리 정의된 Symbol이 있습니다. 각각의 Well-known SymbolSymbol.create와 같이 Symbol 객체의 프로퍼티로 표현됩니다.

Well-known Symbol은 아래와 같습니다.

  • Symbol.hasInstance - 객체의 상속을 결정하기 위해 instanceof가 사용하는 메서드.
  • Symbol.isConcatSpreadable - 컬렉션이 Array.prototype.concat()에 파라미터로 전달되면 Array.prototype.concat()이 컬렉션의 요소를 flat하게 해야한다는 것을 나타내는 boolean 값.
  • Symbol.iterator - Iterator를 반환하는 메서드. (Iterator는 7 장에서 다룹니다.)
  • Symbol.match - 문자열을 비교하기 위해 String.prototype.match()에 의해 사용되는 메서드.
  • Symbol.replace - String.prototype.replace()가 substring을 치환하기 위해서 사용하는 메서드.
  • Symbol.search - String.prototype.search()가 substring의 위치를 찾아 내기 위해서 사용하는 메서드.
  • Symbol.species - 파생된(Derived) 객체를 만들기위한 생성자. (Derived 객체에 대해서는 8 장에서 다룹니다.)
  • Symbol.split - 문자열을 분할하기 위해 String.prototype.split()에서 사용하는 메서드.
  • Symbol.toPrimitive - 객체의 Primitive 값 표현을 반환하는 메서드.
  • Symbol.toStringTag - Object 설명을 생성하기 위해서 Object.prototype.toString()에 의해 사용되는 문자열.
  • Symbol.unscopables - with 문에 포함되어서는 안되는 프로퍼티가 객체 프로퍼티의 이름인 객체.

흔히 사용되는 Well-known Symbol은 다음 절에서 논의하고 나머지는 책의 나머지 전체에서 논의합니다.

정의된 메서드를 Well-known Symbol로 덮어 쓰는 것은 내부 객체를 외부 객체로 바꾸는 것입니다. 결과적으로 코드에 실제적인 영향은 없으며, 객체 사양을 설명하는 방식이 변경됩니다.

Symbol.hasInstance 프로퍼티

모든 함수는 주어진 객체가 그 함수의 인스턴스인지 아닌지를 결정하는 Symbol.hasInstance 메서드를 가지고 있습니다. 이 메서드는 Function.prototype에 정의되어 모든 함수가 instanceof 프로퍼티에 대한 기본 동작을 상속받으며 메서드는 쓰기가 불가능(nonwritable)하고 설정이 불가능(nonconfigurable)하고 열거가 불가능(nonenumerable)하여 실수로 덮어 쓸수 없습니다.

Symbol.hasInstance 메서드는 하나의 파라미터, 즉 확인할 값만 받아들입니다. 전달된 값이 함수의 인스턴스이면 true를 반환합니다. Symbol.hasInstance가 어떻게 작동하는지 이해하기 위해 다음 코드를 살펴보겠습니다.

1
obj instanceof Array;

이 코드는 다음과 같습니다.

1
Array[Symbol.hasInstance](obj);

ECMAScript 6은 근본적으로 instanceof 연산자를 메서드 호출의 축약 구문으로 재정의했습니다. 메서드 호출로 변경되었기 때문에 여러분이 실제로 instanceof가 어떻게 작동하는지 원하는데로 바꿀 수 있습니다.

예를 들어, 객체를 인스턴스로 요구하지 않는 함수를 정의한다고 가정합니다. Symbol.hasInstance의 반환 값을 false로 하드 코딩하면 다음과 같이할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
function MyObject() {
// ...
}
Object.defineProperty(MyObject, Symbol.hasInstance, {
value: function(v) {
return false;
}
});
let obj = new MyObject();
console.log(obj instanceof MyObject); // false

쓰기가 불가능한(nonwritable) 프로퍼티를 덮어 쓰려면 Object.defineProperty()를 사용 해야합니다. 그래서 이 예제는 그 메서드를 사용하여Symbol.hasInstance 메서드를 새로운 함수로 덮어 씁니다. 새로운 함수는 항상 false를 반환하기 때문에 obj가 실제로 MyObject 클래스의 인스턴스이더라도 instanceof 연산자는 Object.defineProperty()호출 후에 false를 반환합니다.

물론 여러분은 값을 검사하고 임의의 조건을 기반으로 값을 인스턴스로 간주해야하는지 여부를 결정할 수도 있습니다. 예를 들어, 1과 100 사이의 값을 가진 숫자는 특별한 number 타입의 인스턴스로 간주됩니다. 이 동작을 수행하기 위해 다음과 같이 코드를 작성할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function SpecialNumber() {
// empty
}
Object.defineProperty(SpecialNumber, Symbol.hasInstance, {
value: function(v) {
return (v instanceof Number) && (v >=1 && v <= 100);
}
});
let two = new Number(2),
zero = new Number(0);
console.log(two instanceof SpecialNumber); // true
console.log(zero instanceof SpecialNumber); // false

이 코드는 값이 Number의 인스턴스이고 또한 1과 100 사이의 값을 가지면 true를 리턴하는 Symbol.hasInstance 메서드를 정의합니다. 따라서 SpecialNumber 함수와 two 변수 사이에 직접 정의된 관계가 없더라도 SpecialNumbertwo를 인스턴스로 요구합니다. instanceof의 왼쪽 피연산자는 Symbol.hasInstance 호출을 트리거하는 객체여야합니다. 왜냐하면 객체가 아니면 instanceof가 항상 단순히 false를 반환하도록 해야하기 때문입니다.

또한 DateError 함수와 같은 모든 내장 함수에 대한 기본 Symbol.hasInstance 프로퍼티을 덮어 쓸 수 있습니다. 그러나 코드에 미치는 영향이 예기치 않게 혼동될 수 있기 때문에 권장하지 않습니다. 자신의 함수에 대해서만 Symbol.hasInstance를 덮어 쓰는 것이 좋은 생각입니다.

Symbol.isConcatSpreadable Symbol

JavaScript Array는 두개의 Array을 연결하기 위해 concat() 메서드를 가지고 있습니다. 다음 예제를 살펴 보겠습니다.

1
2
3
4
5
let colors1 = [ "red", "green" ],
colors2 = colors1.concat([ "blue", "black" ]);
console.log(colors2.length); // 4
console.log(colors2); // ["red","green","blue","black"]

이 코드는 새로운 Arraycolors1의 끝에 연결하여 colors2를 생성합니다. 생성된 Array는 두 Array의 모든 항목을 갖는 Array입니다. 그러나 concat() 메서드는 Array이 아닌 파라미터도 받아 들일 수 있으며, 이 경우 그 파라미터는 단순히 Array의 끝에 추가됩니다.

1
2
3
4
5
let colors1 = [ "red", "green" ],
colors2 = colors1.concat([ "blue", "black" ], "brown");
console.log(colors2.length); // 5
console.log(colors2); // ["red","green","blue","black","brown"]

여기에서 여분의 파라미터 "brown"concat()에 전달되고 colors2 Array의 다섯 번째 항목이됩니다. Array 파라미터가 문자열 파라미터와 다르게 취급되는 이유는 무엇일까요? JavaScript 사양에서는 Array가 자동으로 개별 항목으로 분리되고 다른 타입은 자동으로 분리되지 않는다고 말합니다. ECMAScript 6 이전에는 이 동작을 조정할 방법이 없었습니다.

Symbol.isConcatSpreadable 프로퍼티는 객체가 length 프로퍼티와 숫자 키를 가지고 있으며 숫자 프로퍼티 값이 concat() 호출의 결과에 개별적으로 추가되어야 함을 나타내는 boolean 값입니다. 다른 Well-known Symbol과 달리 이 Symbol 프로퍼티는 기본적으로 표준 객체에 나타나지 않습니다. 대신 Symbol은 특정 타입의 객체에서 concat()이 어떻게 동작 하는지를 보완하는 방법으로 사용할 수 있어 기본 동작을 효과적으로 만듭니다. 다음과 같이 Arrayconcat() 호출에서와 같이 동작하도록 모든 타입을 정의할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
let collection = {
0: "Hello",
1: "world",
length: 2,
[Symbol.isConcatSpreadable]: true
};
let messages = [ "Hi" ].concat(collection);
console.log(messages.length); // 3
console.log(messages); // ["Hi","Hello","world"]

이 예제의 collection 객체는 length 프로퍼티와 두 개의 숫자 키를 가지고 있어 Array 처럼 보이도록 설정되어 있습니다. Symbol.isConcatSpreadable 프로퍼티는 true로 설정되어 프로퍼티 값이 Array의 개별 항목으로 추가되어야 함을 나타냅니다. collectionconcat() 메서드에 전달 될 때, 결과 Array"Hello""world"가 분리되어 "Hi" 엘리먼트 다음에 나타납니다.

concat() 호출로 항목이 분리되지 않도록 Array 서브 클래스에서 Symbol.isConcatSpreadablefalse로 설정할 수도 있습니다. 서브 클래스는 8 장에서 논의합니다.

Symbol.match, Symbol.replace, Symbol.search, 그리고 Symbol.split Symbol들

문자열과 정규 표현식은 JavaScript에서 밀접한 관계가 있습니다. 특히 문자열 타입에는 정규 표현식을 파라미터로 사용하는 여러 가지 메서드가 있습니다.

  • match(regex) - 주어진 문자열이 정규 표현식과 일치하는지 여부를 판별합니다.
  • replace(regex, replacement) - 정규 표현식에 매치된 문자열을 replacement 로 대체합니다.
  • search(regex) - 문자열 내에서 정규 표현식과 일치하는 문자열을 찾습니다.
  • split(regex) - 문자열을 정규 표현식과 일치하는 문자열 Array로 나눕니다.

ECMAScript 6 이전에는 이러한 메서드가 정규 표현식과 상호 작용하는 방식이 개발자에게 숨겨져 있어 개발자가 정의한 객체에 정규 표현식을 사용할 수 없었습니다. ECMAScript 6은 이러한 네 가지 메서드에 해당하는 네 개의 Symbol을 정의하여 네이티브 동작을 RegExp 내장 객체에 효과적으로 아웃소싱할 수 있습니다.

Symbol.match, Symbol.replace, Symbol.searchSymbol.split Symbol은 정규 표현식 파라미터에 대한 match() ,replace(), search(), split () 메서드 각각에 대한 첫 번째 파라미터에서 호출되어야 합니다. 네 개의 Symbol 프로퍼티는 RegExp.prototype에 문자열 메서드가 사용해야하는 기본 구현으로 정의됩니다.

이것을 알면 정규 표현식과 비슷한 방식으로 문자열 메서드에 사용할 객체를 만들 수 있습니다. 그렇게하기 위해 코드에서 다음과 같은 Symbol 함수를 사용할 수 있습니다.

  • Symbol.match - 문자열 파라미터를 받아들이고 일치하는 Array를 반환하는 함수. 일치하는 것이 없으면 null입니다.
  • Symbol.replace - 문자열 파라미터와 대체 문자열을 받아들이고 문자열을 반환하는 함수입니다.
  • Symbol.search - 문자열 파라미터를 받아들이고 일치 항목의 숫자 인덱스를 반환하는 함수입니다. 일치하는 항목이 없으면 -1을 반환합니다.
  • Symbol.split - 문자열 파라미터를 받아들이고 일치하는 문자열을 포함하는 Array를 반환하는 함수입니다.

객체에 이러한 프로퍼티를 정의할 수 있으므로 정규 표현식 없이 패턴 일치를 구현하는 객체를 만들고 정규 표현식을 필요로하는 메서드에서 사용할 수 있습니다. 다음은 이러한 Symbol이 실제로 작동하는 것을 보여주는 예입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// effectively equivalent to /^.{10}$/
let hasLengthOf10 = {
[Symbol.match]: function(value) {
return value.length === 10 ? [value] : null;
},
[Symbol.replace]: function(value, replacement) {
return value.length === 10 ? replacement : value;
},
[Symbol.search]: function(value) {
return value.length === 10 ? 0 : -1;
},
[Symbol.split]: function(value) {
return value.length === 10 ? ["", ""] : [value];
}
};
let message1 = "Hello world", // 11 characters
message2 = "Hello John"; // 10 characters
let match1 = message1.match(hasLengthOf10),
match2 = message2.match(hasLengthOf10);
console.log(match1); // null
console.log(match2); // ["Hello John"]
let replace1 = message1.replace(hasLengthOf10, "Howdy!"),
replace2 = message2.replace(hasLengthOf10, "Howdy!");
console.log(replace1); // "Hello world"
console.log(replace2); // "Howdy!"
let search1 = message1.search(hasLengthOf10),
search2 = message2.search(hasLengthOf10);
console.log(search1); // -1
console.log(search2); // 0
let split1 = message1.split(hasLengthOf10),
split2 = message2.split(hasLengthOf10);
console.log(split1); // ["Hello world"]
console.log(split2); // ["", ""]

hasLengthOf10 객체는 문자열 길이가 정확히 10일 때마다 일치하는 정규 표현식처럼 작동합니다. hasLengthOf10에있는 네 개의 메서드는 각각 적절한 Symbol 을 사용하여 구현되고 두 문자열에 상응하는 메서드가 호출됩니다. 첫 번째 문자열인 message1은 11 개의 문자를 가지므로 일치하지 않습니다. 두 번째 문자열 message2는 10 개의 문자를 가지므로 일치합니다. 정규식이 아니더라도 hasLengthOf10은 각 문자열 메서드에 전달되고 추가 메서드로 인해 올바르게 사용됩니다.

이것은 간단한 예제지만 보다 복잡한 매칭를 수행하는 기능도 가능합니다. 그리고 현재 정규 표현식으로 가능했던 것보다 커스텀 패턴 매처에 대한 많은 가능성을 열어줍니다.

Symbol.toPrimitive 메서드

JavaScript는 특정 작업을 적용할 때 객체를 Primitive 값으로 암시적으로 변환하려고 시도합니다. 예를 들어 문자열을 double equals (==) 연산자를 사용하여 객체와 비교하면 비교하기 전에 객체가 Primitive 값으로 변환됩니다. 정확하게 어떤 Primitive 값이 사용되어야 하는가는 이전에는 내부 연산 이었지만, ECMAScript 6에서는 Symbol.toPrimitive 메서드를 통해 그 값을 밖으로 노출시킵니다.

Symbol.toPrimitive 메서드는 각 표준 타입의 프로토 타입에 정의되어 있으며, 객체가 Primitive로 변환 될 때 어떻게되어야 하는지를 규정합니다. Primitive 변환이 필요할 때, Symbol.toPrimitive는 하나의 파라미터를 가지고 호출되며, 명세서에서 hint라고 설명합니다. hint 파라미터는 세 개의 문자열 값 중 하나입니다. hint"number"이면 Symbol.toPrimitive는 number를 반환해야합니다. hint"string"이면 string이 반환되어야하고, “default”이면 해당 연산은 그 타입에 대한 선호도가 없습니다.

대부분의 표준 객체에서 number 모드는 우선 순위에 따라 다음과 같은 동작을합니다.

  1. valueOf() 메서드를 호출해, 결과가 Primitive인 경우는 그 값을 돌려 준다.
  2. 그렇지 않은 경우는,toString() 메서드를 호출해, 결과가 Primitive인 경우는 그 값을 돌려 준다.
  3. 그렇지 않으면 오류를 발생시킵니다.

마찬가지로 대부분의 표준 객체에서 string 모드의 동작은 다음과 같은 우선 순위를 갖습니다.

  1. toString() 메서드를 호출해, 결과가 Primitive인 경우는 그 값을 돌려 준다.
  2. 그렇지 않은 경우는, valueOf() 메서드를 호출해, 결과가 Primitive인 경우는 그 값을 돌려 준다.
  3. 그렇지 않으면 오류를 발생시킵니다.

대부분의 경우 표준 객체는 Default 모드를 number 모드와 동일하게 취급합니다 (Default 모드를 string 모드와 동일하게 취급하는 Date제외). Symbol.toPrimitive 메서드를 정의함으로써, 여러분은 이 Default 모드를 오버라이드 할 수 있습니다.

Default 모드는 ==연산자, +연산자 및 Date 생성자에 단일 파라미터를 전달할 때만 사용됩니다. 대부분의 작업에는 string 또는 number 모드가 사용됩니다.

Default 변환 행동을 무시하려면 아래 예제 처럼 Symbol.toPrimitive를 사용하고 함수를 값으로 지정하십시오.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function Temperature(degrees) {
this.degrees = degrees;
}
Temperature.prototype[Symbol.toPrimitive] = function(hint) {
switch (hint) {
case "string":
return this.degrees + "\u00b0"; // degrees symbol
case "number":
return this.degrees;
case "default":
return this.degrees + " degrees";
}
};
let freezing = new Temperature(32);
console.log(freezing + "!"); // "32 degrees!"
console.log(freezing / 2); // 16
console.log(String(freezing)); // "32째"

위의 예제는 Temperature 생성자를 정의하고 프로토타입에 대한 기본 Symbol.toPrimitive 메서드를 오버라이드합니다. hint 파라미터가string 모드,number 모드 또는 Default 모드 (JavaScript 엔진에 의해 hint 파라미터로 채워짐)를 나타내는지에 따라 다른값이 리턴됩니다. string 모드에서, Symbol.toPrimitive 메서드는 유니 코드 Symbol로 온도를 반환합니다. number 모드에서는 숫자값만 반환하고, Default 모드에서는 숫자 뒤에 "degrees"라는 단어를 추가합니다.

각각의 로그문은 다른 hint 파라미터를 트리거합니다. +연산자는 hint"default"로 설정함으로써 Default 모드를 트리거하고, / 연산자는 hint"number"로 설정함으로써 number 모드를 트리거하고, String() 함수는 hint"string"으로 설정함으로써 string 모드로 트리거합니다. 세 가지 모드 모두에 대해 다른 값을 반환하는 것이 가능합니다. 하지만 Default 모드를 string 또는 number 모드와 동일하게 설정하는 것이 훨씬 더 일반적입니다.

Symbol.toStringTag Symbol

JavaScript에서 가장 흥미로운 문제중 하나는 여러 글로벌 실행환경을 사용할 수 있다는 것입니다. 이는 페이지에 iframe이 포함될 때 웹 브라우저에서 발생합니다. 페이지와 iframe에는 각각 자체 실행 환경이 있기 때문입니다. 대부분의 경우 데이터를 주고받을 수 있으므로 문제가되지 않습니다. 하지만 문제는 객체가 다른 객체에 전달된 후 처리할 객체의 타입을 식별하려고 할 때 발생합니다.

이 문제의 일반적인 예는 iframe의 Array의 포함 페이지로 또는 그 반대로 전달하는 것입니다. ECMAScript 6 용어에서 iframe 및 포함 페이지는 각각 JavaScript의 실행 환경인 다른 영역(realm)을 나타냅니다. 각 영역에는 전역 객체의 자체 사본이 있는 고유한 전역 Scope이 있습니다. Array는 생성되는 영역에 관계없이 Array이어야 합니다. 그러나 다른 영역으로 넘어 갔을 때 Array가 이전 영역의 생성자로 만들어졌고 Array가 현재 영역의 생성자를 나타내므로 instanceof Array 호출은 false를 반환합니다.

식별 문제에 대한 해결 방법

이 문제에 직면한 개발자들은 곧 Array를 식별하는 좋은 방법을 발견했습니다. 그들은 객체에 대해 표준 toString()메서드를 호출하면 항상 예측 가능한 문자열이 반환된다는 것을 발견했습니다. 그래서 많은 JavaScript 라이브러리는 다음과 같은 함수를 포함하기 시작했습니다.

1
2
3
4
5
function isArray(value) {
return Object.prototype.toString.call(value) === "[object Array]";
}
console.log(isArray([])); // true

이 방법이 조금 어색해 보일지 모르지만 모든 브라우저에서 Array를 식별하는 데는 매우 효과적입니다. ArraytoString() 메서드는 객체를 포함하는 문자열 표현을 반환하기 때문에 객체 식별에 유용하지 않습니다. 그러나 Object.prototype에 대한 toString() 메서드는 quirk를 가지고 있습니다 : 반환된 결과에 [[Class]]로 불리는 내부적으로 정의된 이름을 포함합니다. 개발자는 객체에서 이 메서드를 사용하여 JavaScript 환경에서 객체의 데이터 타입이 무엇이라고 생각 하는지를 검색할 수 있습니다.

개발자는 이 동작을 변경할 방법이 없었기 때문에 동일한 방법을 사용하여 Native 객체와 개발자가 만든 객체를 구별할 수 있다는 것을 신속하게 깨달았습니다. 가장 중요한 경우는 ECMAScript 5 JSON 객체입니다.

ECMAScript 5 이전에는 많은 개발자들이 Douglas Crockford의 json2.js를 사용하여 글로벌 JSON 객체를 생성했습니다. 하지만 브라우저가 JSON 전역 객체를 구현하기 시작하면서, JavaScript 환경에서 또는 다른 라이브러리를 통해 제공되는 전역 JSON이 필요하다는 것을 깨달았습니다. isArray() 함수에서 보여준 것과 같은 기술을 사용하여 많은 개발자들이 다음과 같은 함수를 만들었습니다.

1
2
3
4
function supportsNativeJSON() {
return typeof JSON !== "undefined" &&
Object.prototype.toString.call(JSON) === "[object JSON]";
}

개발자가 iframe 경계를 넘어서 Array를 식별할 수있게 해주는 Object.prototype과 동일한 특성을 사용해 JSON이 기본 JSON 객체인지 여부를 알 수있는 방법을 제공합니다. 기본이 아닌 JSON 객체는 [object Object]를 반환하지만 Native 버전은 [object JSON]을 반환합니다. 이 접근법은 Native 객체를 식별하기 위한 사실상의 표준이되었습니다.

ECMAScript 6의 해결책

ECMAScript 6은 Symbol.toStringTag Symbol을 통해 이 동작을 재정의합니다. 이 SymbolObject.prototype.toString.call()이 호출될 때 생성되어야 하는 값을 정의하는 각 객체의 프로퍼티를 나타냅니다. Array의 경우 함수가 반환하는 값은 Symbol.toStringTag 프로퍼티에 "Array"를 저장하여 설명합니다.

마찬가지로, 자신의 객체에 대한 Symbol.toStringTag 값을 정의할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
function Person(name) {
this.name = name;
}
Person.prototype[Symbol.toStringTag] = "Person";
let me = new Person("Nicholas");
console.log(me.toString()); // "[object Person]"
console.log(Object.prototype.toString.call(me)); // "[object Person]"

이 예제에서 Symbol.toStringTag 프로퍼티는 Person.prototype에 정의되어 문자열 표현을 생성하기 위한 기본 동작을 제공합니다. Person.prototypeObject.prototype.toString() 메서드를 상속 받기 때문에 Symbol.toStringTag에서 반환된 값은 me.toString() 메서드를 호출할 때도 사용됩니다. 그러나 Object.prototype.toString.call() 메서드의 사용에 영향을 미치지 않고 다른 동작을 제공하는 자신만의 toString() 메서드를 정의할 수 있습니다. 다음과 같이 사용할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Person(name) {
this.name = name;
}
Person.prototype[Symbol.toStringTag] = "Person";
Person.prototype.toString = function() {
return this.name;
};
let me = new Person("Nicholas");
console.log(me.toString()); // "Nicholas"
console.log(Object.prototype.toString.call(me)); // "[object Person]"

이 코드는 Person.prototype.toString()을 정의하여 name 프로퍼티 값을 반환합니다. Person 인스턴스가 더 이상 Object.prototype.toString() 메서드를 상속하지 않기 때문에 me.toString()을 호출하면 다른 행동을 보입니다.

달리 명시하지 않는한 모든 객체는 Object.prototype에서 Symbol.toStringTag을 상속받습니다. "Object"문자열이 기본 프로퍼티 값입니다.

개발자가 정의한 객체에서 Symbol.toStringTag에 어떤 값을 사용할 수 있는지에 대한 제한은 없습니다. 예를 들어, 다음과 같이 Symbol.toStringTag 프로퍼티의 값으로 "Array"를 사용하지 못하게하는 방법은 없습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Person(name) {
this.name = name;
}
Person.prototype[Symbol.toStringTag] = "Array";
Person.prototype.toString = function() {
return this.name;
};
let me = new Person("Nicholas");
console.log(me.toString()); // "Nicholas"
console.log(Object.prototype.toString.call(me)); // "[object Array]"

Object.prototype.toString()을 호출 한 결과는 이 코드에서 "[object Array]"이며 실제 Array에서 얻은 결과와 같습니다. 이것은 Object.prototype.toString()이 더 이상 객체 타입을 식별하는 완전히 신뢰할 수있는 방법이 아니라는 사실을 강조합니다.

Native 객체에 대한 문자열 태그 변경도 가능합니다. 다음과 같이 객체의 프로토 타입에 Symbol.toStringTag을 할당하면 됩니다.

1
2
3
4
5
Array.prototype[Symbol.toStringTag] = "Magic";
let values = [];
console.log(Object.prototype.toString.call(values)); // "[object Magic]"

이 예제에서 Symbol.toStringTagArray에 대해 덮어 쓰여지더라도 Object.prototype.toString()을 호출하면 대신 [[Object Magic]]이 됩니다. 이런 방식으로 내장 객체를 변경하지 말 것을 권고 하지만 JavaScript에서 이것을 금지하지는 않습니다.

Symbol.unscopables Symbol

with문은 JavaScript에서 가장 논쟁의 여지가 있는 부분중 하나입니다. 원래 반복적인 타이핑을 피하도록 설계된 with 문은 나중에 코드를 이해하기 어렵게 만들고 성능에 부정적이고 오류가 발생하기 쉬워서 많은 비난을 받고 있습니다.

결과적으로 with 문은 strict 모드에서는 허용되지 않습니다. 이러한 제한은 클래스와 모듈에도 영향을 미칩니다. 클래스와 모듈은 기본적으로 strict 모드이며 opt-out이 없습니다.

미래의 코드는 의심할 여지없이 with 문을 사용하지 않지만, ECMAScript 6는 하위 호환성을 위한 nonstrict 모드를 여전히 지원하며, with를 사용하는 코드가 계속해서 제대로 작동하도록 하는 방법을 찾아야만 합니다.

이 작업의 복잡성을 이해하기 위해 다음 코드를 살펴보겠습니다.

1
2
3
4
5
6
7
8
9
10
let values = [1, 2, 3],
colors = ["red", "green", "blue"],
color = "black";
with(colors) {
push(color);
push(...values);
}
console.log(colors); // ["red", "green", "blue", "black", 1, 2, 3]

이 예제에서, with문 안에서 push()를 두 번 호출하면 colors.push()와 동등합니다. 왜냐하면 with 문은 push를 로컬 바인딩으로 추가했기 때문입니다. color의 참조는 values 참조처럼 with문 밖에서 생성된 변수를 참조합니다.

ECMAScript 6는 Arrayvalues 메서드를 추가했습니다. (values 메서드에 대해서는 7 장 “Iterator와 Generator”에서 자세히 설명합니다.) 즉, ECMAScript 6 환경에서 with문 내의 values 참조는 지역 변수 values를 참조하는 것이 아니라 코드를 깨뜨릴 수 있는 Arrayvalues 메서드를 참조해야합니다. 이것이 Symbol.unscopables Symbol이 존재하는 이유입니다.

Symbol.unscopables 심볼은 Array.prototype에 사용되어 어떤 프로퍼티가 with문 안에서 바인딩을 생성해서는 안된다는 것을 나타냅니다. 현재 존재하는 Symbol.unscopableswith 명령문 바인딩을 생략하고 valuestrue 인 블록을 시행하기 위한 식별자를 키로 가지는 객체입니다. 다음은 Array에 대한 기본 Symbol.unscopables 프로퍼티입니다.

1
2
3
4
5
6
7
8
9
10
// built into ECMAScript 6 by default
Array.prototype[Symbol.unscopables] = Object.assign(Object.create(null), {
copyWithin: true,
entries: true,
fill: true,
find: true,
findIndex: true,
keys: true,
values: true
});

Symbol.unscopables 객체는 Object.create(null) 호출에 의해 생성되고 ECMAScript 6에 있는 새로운 Array 메서드들을 모두 포함하는 null 프로토 타입을 가지고 있습니다. (이 메서드들은 7장 “Iterator와 Generator”및 9장 “Arrays.”에서 설명합니다.) 이러한 메서드에 대한 바인딩은 with 문 내에 만들어지지 않으므로 이전 코드가 아무런 문제없이 계속 작동할 수 있습니다.

일반적으로 with 문을 사용하지 않고 코드베이스의 기존 객체를 변경하지 않는한 객체에 Symbol.unscopables을 정의할 필요가 없습니다.

Summary

Symbol은 JavaScript에서 새로운 유형의 Primitive 타입이며 Symbol을 참조하지 않고는 액세스할 수 없는 프로퍼티를 만드는데 사용됩니다.

진정한 Private은 아니지만 이러한 프로퍼티는 실수로 변경하거나 덮어 쓰기가 어렵기 때문에 개발자로부터 일정 수준의 보호가 필요한 기능에 적합합니다.

Symbol 값을 쉽게 식별할 수 있도록 Symbol에 대한 설명을 제공할 수 있습니다. 동일한 설명을 사용하여 코드의 다른 부분에서 공유 Symbol을 사용할 수 있는 전역 Symbol 레지스트리가 있습니다. 이런 식으로 여러 장소에서 같은 이유로 동일한 Symbol을 사용할 수 있습니다.

Object.keys() 또는 Object.getOwnPropertyNames()와 같은 메서드는 Symbol을 반환하지 않고, ECMAScript 6에 새로운 메서드인 Object.getOwnPropertySymbols()가 추가되어 Symbol 프로퍼티를 검색할 수 있습니다. Object.defineProperty()Object.defineProperties ()메서드를 호출하여 Symbol 프로퍼티를 변경할 수 있습니다.

Well-known Symbol은 표준 객체에 대한 이전의 내부 전용 기능을 정의하고 Symbol.hasInstance 프로퍼티와 같이 전역적으로 사용 가능한 Symbol 상수를 사용합니다. 이 Symbol은 스펙에서 접두어 Symbol.을 사용하며 개발자가 다양한 방법으로 표준 객체의 동작을 수정할 수 있도록 합니다.


이 내용은 나중에 참고하기 위해 제가 공부하며 정리한 내용입니다.
의역, 오역, 직역이 있을 수 있음을 알려드립니다.
This post is a translation of this original article [https://leanpub.com/understandinges6/read#leanpub-auto-symbols-and-symbol-properties]

참고

공유하기