스펙에 정의된 로직으로부터 명시적으로 참조되어 사용되는 Symbol values
ECMA Specification(링크)의 6.1.5.1 Well-Known Symbols를 살펴보면 알고리즘에 이름을 부여하고 이름으로 참조하기 위한 빌트인 Symbol 값입니다.
@@는 Well-Known Symbol을 나타내는 기호입니다. @@match와 Symbol.match가 같으며 스펙에서는 @@match 형태, 개발자는 Symbol.match 형태를 사용합니다.
match() 메소드를 실행하면 디폴트로 @@match가 실행됩니다. 소스코드에 Symbol.match를 작성하면 @@match가 실행되지 않고, Symbol.match가 우선 실행됩니다.
예를 들어 String.prototype.match가 호출되면 우선 개발자 코드에서 "Symbol.match"를 찾습니다. 성공한다면 해당 함수를 실행합니다. 하지만, 찾지 못했을 경우 엔진에서 @@match를 실행합니다.
원래, 기존에는 "Symbol.XXXX"가 따로 제공되지 않았기에 바로 엔진에서 @@XXXX 알고리즘이 호출되어 실행되었습니다. 하지만 이제는 개발자가 운용(변경, 추가)할 수 있도록 오픈되었습니다. 그리고 이것이 Symbol.property의 목적입니다.
개발자 코드로 디폴트 기능을 유용성을 가할 수 있도록 오버라이딩 할 수 있습니다.
여기서 Symbol 항목이 개발자가 작성하는 코드들입니다. 대응 부분의 함수 및 알고리즘을 실행하면 우선 Symbol 항목에 관련 내용이 오버라이드 되어있으면 그 코드를 실행하고, 없으면 엔진의 기본 알고리즘을 호출합니다.
Object.prototype.toString()의 확장
기본적으로 Object에서 비롯된 모든 객체에 연결되는 메소드인 toString()은 호출 시 [object type]을 반환합니다. 여기서 type은 Object Type을 의미하는데, 기본적인 toString()을 호출하면 [object Object] 를 반환합니다.
const Sports = function(){}; const obj = new Sports(); console.log(obj.toString()); console.log({}.toString()); // [object Object] // [object Object]
클래스나 인스턴스의 타입을 명확하게 구분하기 힘들기에 Symbol.toStringTag를 이용해 오버라이드하여 구분이 가능하게 할 수 있습니다. 반환될 값의 두 번째에 표시될 문자열을 작성할 수 있습니다. ex) [object XXX]
const Sports = function(){}; const obj = new Sports(); console.log(obj.toString()); Sports.prototype[Symbol.toStringTag] = "농구"; console.log(obj.toString()); // [object Object] // [object 농구]
Symbol.toStringTag로 @@toString를 오버라이드해서 코드에서 필요로 하는 형태로 인스턴스 타입을 구분할 수 있습니다.
Array.prototype.concat()의 확장
concat()의 대상이 Array일 때와 Array-Like일 때가 다릅니다. 디폴트 값에 유의해야합니다.
concat() 메소드가 대상이 배열이면 전개하여 연결하는 것이 디폴트입니다. @@isConcatSpreadable를 true로 처리
const one = [10, 20], two = ["A", "B"]; const show = () => { console.log(one.concat(two)); }; show(); two[Symbol.isConcatSpreadable] = true; show(); two[Symbol.isConcatSpreadable] = false; show(); // [10, 20, A, B] // [10, 20, A, B] // [10, 20, [A, B]]
concat() 메소드가 대상이 Array-Like이면 전개하지 않고 연결하는 것이 디폴트입니다. @@isConcatSpreadable를 false로 처리
const one = [10, 20]; const like = {0: "A", 1: "B", length: 2}; const show = () => { console.log(one.concat(like)); }; show(); like[Symbol.isConcatSpreadable] = true; show(); like[Symbol.isConcatSpreadable] = false; show();
const obj = [1, 2, 3]; const one = obj.slice(1, 3); const two = one.slice(1, 2);
⇒ Array 인스턴스의 메소드를 호출하면 값을 반환하는 것이 아닙니다.
⇒ 반환할 Array 인스턴스를 생성하고 메소드에서 구한 값을 반환할 Array 인스턴스에 설정하여 이를 반환합니다.
class Sports extends Array {}; const obj = new Sports(10, 20, 30); const one = obj.slice(1, 2); console.log(one); // [20]
일반적으로 new 연산자로 클래스/생성자 함수를 호출하면 prototype에 연결된 constructor가 호출되고 인스턴스를 생성해서 반환합니다.
인스턴스의 메소드를 호출해서 반환하면 인스턴스가 반환되는데 obj는 인스턴스이기에 prototype이 없고 constructor가 없는데 어떻게 인스턴스를 만들수 있는가?
⇒ symbol.species가 constructor를 불러주는 역할을 수행합니다
Symbol.species는 static access property이며 getter만 있고 setter는 없습니다.
class Sports extends Array { static get [Symbol.species]() { return Array; } }; const obj = new Sports(10, 20)
Symbol.species를 사용할 수 있는 빌트인 오브젝트
빌트인 오브젝트를 상속받은 class에 Symbol.species를 작성하면 빌트인 오브젝트의 @@species가 오버라이드 됩니다.
class Sports extends Array { static get [Symbol.species](){ return Array; } }; const one = new Sports(10, 20, 30); console.log(one instanceof Sports); const two = one.slice(1, 2); console.log(two instanceof Array); console.log(two instanceof Sports); // true // true // false
one의 경우
log(one instanseOf Array) // true
log(one instanseOf Sports) // true
Array를 상속받은 Sports 클래스로 생성하여 Sports.prototype.constructor를 호출하여 prototype으로 인스턴스를 생성합니다. 때문에, Symbol.species가 동작하지 않습니다.
two의 경우
log(two instanseOf Array) // true
log(two instanseOf Sports) // false
slice() 메소드는 결과값만 전달하는 것이 아닌 @@species가 동작되어 결과로 인스턴스를 생성하여 반환하는 것입니다. 따라서, 메소드의 결과값을 담을 인스턴스를 생성할때에만 @@species를 오버라이딩한 Symbol.species가 동작합니다.
class Sports extends Array의 static access 프로퍼티의 getter로 [Symbol.species]를 작성하였기 때문에 오버라이딩 됩니다. 따라서, @@species가 아닌 Array Object의 constructor로 생성된 값을 반환 받은 것 입니다.
오버라이딩 하지 않은 경우
log(two instanseOf Array) // true
log(two instanseOf Sports) // true
class Sports extends Array {};의 경우 defalut로 @@species가 동작하며, class Sports extends Array로 생성한 인스턴스로 반환됩니다.
오브젝트를 대응하는 Primitive 값으로 변환
스펙을 잘 살펴보면 Symbol.toPrimitive의 초기값은 well-known symbol인 @@toPrimitive라고 정의하고 있습니다. 이 프로퍼티는 { [[Writable]]: false, [[Enumerable]]: false, [[Configurable]]: false }으로 작성, 열거, 설정이 모두 불가능합니다.
그리고, 엔진에서 toPrimitive를 대응, 기대하는 타입으로는 number, string, default가 있습니다. 매개변수로 hint
를 받는데 해당 타입에 따라 primitive값을 적절히 재정의하여 반환할 수 있습니다.
const point = {bonus: 100}; console.log(point.toString()); const book = { toString() { return "책" } }; console.log(`${book}`); // [object Object] // 책
const point = {bonus: 100}; console.log(point.valueOf()); const book = { toString() { return 70 }, valueOf() { return 30 } }; console.log(book * 20); // {bonus: 100} // 600
Symbol.toPrimitive는 오브젝트를 Primitive 값으로 변환하는데 있어서 기본엔진보다 편의성을 높일 수 있게 해줍니다. hint
를 이용하면 하나의 메소드로 반환값을 유연하게 지정해줄 수 있습니다.
const obj = { [Symbol.toPrimitive](hint){ return hint === "number" ? 30 : hint === "string" ? "책" : "default"; } }; console.log(20 * obj); console.log(`${obj}` + 100); console.log(obj + 50); console.log("default" == obj); // 600 // 책100 // default50 // true
Symbol.toPrimitive값을 오버라이드해서 사용하며 hint는 해당 오브젝트를 호출하는 상황에 따라 맞게 들어옵니다.
const list = [10, 20]; const obj = list[Symbol.iterator](); console.log(obj.next()); console.log(obj.next()); console.log(obj.next()); // {value: 10, done: false} // {value: 20, done: false} // {value: undefined, done: true}
const list = "1A"; const obj = list[Symbol.iterator](); console.log(obj.next()); console.log(obj.next()); console.log(obj.next()); console.log(obj.next()); // {value: "A", done: false} // {value: "B", done: false} // {value: "C", done: false} // {value: undefined, done: true}
const obj = { [Symbol.iterator](){ return { // 클로저 count: 0, maxCount: this.maxCount, // 이터레이터 프로토콜 오버라이드 next(){ if (this.count < this.maxCount){ return {value: this.count++, done: false}; }; return {value: undefined, done: true}; } }; } }; obj.maxCount = 2; for (const value of obj){ console.log(value); }; // 0 // 1
[Symbol.iterator](){return {...}}
⇒ 빌트인 Object인 obj는 [Symbol.iterator]가 정의되어 있지 않기에 사용자 정의 이터레이터를 정의합니다.
⇒ 이터레이터 오브젝트를 사용할 때 호출하는 메소드인 next()를 구현합니다. 로직에 맞게 특정 조건까지 반환 오브젝트의 done을 false로 해 반복하게끔 하고, 반복이 종료되면 done:true를 반환해 이터레이터의 반복이 끝나도록 합니다.
⇒ maxCount를 설정해 maxCount까지 초기값이 0인 count가 도달할 때까지 순회하도록 반복하는 로직입니다.
Symbol.iterator는 Iterator 오브젝트를 반환하여 반복가능하게 해주고, generator 함수는 yield를 수행해 호출할 때마다 값을 반환해줍니다.
따라서, Object{}에 [Symbol.iterator] 에 generator 함수를 연결하면 반복할 때마다 yield 수행하게 됩니다.
const obj = {}; obj[Symbol.iterator] = function*(){ yield 1; yield 2; yield 3; }; console.log([...obj]); // [1, 2, 3]
Symbol.iterator의 __proto__에 제너레이터 오브젝트가 있는 구조
할당 연산자 = 때문에 obj의 Symbol.iterator가 제너레이터로 대체되는 것 같지만 그 것이 아닌 __proto__에 제너레이터 오브젝트에 연결되는 구조로 이렇게 두 개가 존재해야 iterator와 yield 처리가 둘 다 가능합니다.
제너레이터 오브젝트에 이터레이터 오브젝트를 대체하는게 아닌 연결하는 것이기에 값을 따로 설정되는게 아닌 공유되는 형태입니다.
const gen = function*(){ yield 10; yield 20; }; const genObj = gen(); console.log(genObj.next()); const obj = genObj[Symbol.iterator](); console.log(obj.next()); // {value: 10, done: false} // {value: 20, done: false}
1.const genObj = gen();
⇒ gen() 제너레이터 함수를 호출하면 제너레이터 오브젝트가 반환되는데, 이는 곧 이터레이터이기도 해서 next() 메소드를 호출하면 첫 번째 yield가 실행되어 {value: 10, done: false}이 반환됩니다.
2. const obj = genObj[Symbol.iterator]();
⇒ 제너레이터 오브젝트의 Symbol.iterator()를 호출하여 이터레이터 오브젝트를 반환합니다.
3. console.log(obj.next());
⇒ obj의 첫 번째 next()호출이기에 첫번째 yield인 10이 반환될 것 같지만 실제로는 {value: 20, done: false}이 반환되어 출력됩니다.
⇒ 이는 제너레이터 함수에서 수행했던 과정부터 시작하여 두 번째 yield가 수행되어 값을 반환되었다는것이고 즉, 이터레이터 오브젝트에서 yield 처리를 공유한다는 점을 알 수 있습니다.
Well-Known Symbol을 지원하는 String 메소드로는 match(), replace(), search(), split() 정도가 있습니다. 기본 값으로는 String.prototype.메소드명으로 연결되어 있습니다.
match()는 String.prototype.match()에 연결되어 있으며 문자열에 패턴을 매치하고 매치된 결과를 배열로 반환합니다.
const result = "Sports".match(/s/); console.log(result); // ["s", index: 5, input: "Sports", groups: undefined]
Symbol.match()
Symbol.match()는 개발자 코드를 함수 블록에 작성합니다. 오버라이딩하여 String.prototype.match()대신 동작하도록 할 수 있습니다.
const sports = { base: "ball", [Symbol.match](value){ return this.base.indexOf(value) < 0 ? "없음" : "있음"; } } console.log("al".match(sports)); // 있음
⇒ base로 설정된 'ball'에 'al'이 있으면 '있음' 없으면 '없음' 을 반환하는 로직인데 엔진은 sports 오브젝트에서 Symbol.match가 오버라이드 되있는지 확인합니다.
⇒ 없다면 디폴트 값인 String.prototype.match()를 호출하고 있으면 Symbol.match()를 호출합니다. 이때 value가 될 매개변수는 "al"이 됩니다.
⇒ 이 때 Symbol.match 메소드를 오버라이드하는 것이기에 메소드의 시맨틱은 유지해야 합니다.
Symbol.match = false
여기서 match를 할 value가 /param/ 이런 패턴이면 파라미터를 패턴으로 처리합니다. 이럴 경우 정규 표현식을 사용할 수 없으므로 에러가 발생하는데 이를 Symbol.match = false 로 설정해서 해결할 수 있습니다.
try { "/book/".startsWith(/book/); } catch { console.log("정규 표현식으로 처리"); }; let check = /book/; check[Symbol.match] = false; console.log("/book/".startsWith(check)); // 정규 표현식으로 처리 // true