[JS] #13 Symbol 프로퍼티

simoniful·2021년 4월 30일
0

ES6 JS - Basic

목록 보기
13/18
post-thumbnail

Well-Known Symbols

스펙에 정의된 로직으로부터 명시적으로 참조되어 사용되는 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 항목에 관련 내용이 오버라이드 되어있으면 그 코드를 실행하고, 없으면 엔진의 기본 알고리즘을 호출합니다.


Symbol.toStringTag

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 농구]
  1. 첫 번째 console.log(obj.toString());
    • 인스턴스 타입을 반환하며 [object Object]가 출력됩니다.
    • function으로 만들었는데 Object가 반환됩니다.
  2. Sports.prototype[Symbol.toStringTag] = "농구";
    • prototype에 Symbol.toStringTag를 연결하고 type에 표시될 문자열을 "농구"로 작성했습니다.
    • function마다 지정할 수 있으므로 자세하게 구분하여 작성할 수 있습니다.
  3. 두 번째 console.log(obj.toString());
    [object 농구]가 출력됩니다.

Symbol.toStringTag로 @@toString를 오버라이드해서 코드에서 필요로 하는 형태로 인스턴스 타입을 구분할 수 있습니다.


Symbol.isConcatSpreadable

Array.prototype.concat()의 확장

concat()의 대상이 Array일 때와 Array-Like일 때가 다릅니다. 디폴트 값에 유의해야합니다.

Array

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() 메소드가 대상이 배열
    one 배열의 끝에 two 배열의 엘리먼트를 하나씩 연결합니다.
  • [Symbol.isConcatSpreadable] = true;
    one 배열의 끝에 two 배열의 엘리먼트를 하나씩 연결합니다.
  • [Symbol.isConcatSpreadable] = false;
    전개하지 않고 two 배열 자체를 연결합니다.

Array-Like(object)

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();
  • concat() 메소드가 대상이 Array-Like
    전개하지 않고 like 오브젝트 자체를 연결합니다.
  • [Symbol.isConcatSpreadable] = true;
    one 배열의 끝에 like 오브젝트의 value를 하나씩 연결합니다.
  • [Symbol.isConcatSpreadable] = false;
    전개하지 않고 like 오브젝트 자체를 연결합니다.

Symbol.species

  • Symbol.species는 constructor를 반환
    • constructor를 실행하면 인스턴스를 생성하여 반환하므로 결국, 인스턴스를 반환하게 됩니다.
  • Symbol.species를 통해 오버라이드하면 다른 인스턴스를 반환할 수 있다는 의미

메소드를 실행한 후의 결과 형태

const obj = [1, 2, 3];
const one = obj.slice(1, 3);
const two = one.slice(1, 2);

  • prototype은 없고 __proto__만 있으므로 obj는 빌트인 Array 오브젝트가 아니라 Array.prototype에 연결된 메소드로 생성한 인스턴스입니다.
  • obj, one, two는 구조는 차이가 없으며 값만 다른데 이것은 인스턴스에 있는 메소드를 호출하면, 메소드 실행 결과값을 반환하는 것이 아니라 결과값이 설정된 인스턴스를 반환하기 때문입니다.
  • 반환된 one으로 메소드를 호출할 수 있다는 것은 one이 인스턴스이기 때문입니다.

⇒ Array 인스턴스의 메소드를 호출하면 값을 반환하는 것이 아닙니다.
⇒ 반환할 Array 인스턴스를 생성하고 메소드에서 구한 값을 반환할 Array 인스턴스에 설정하여 이를 반환합니다.

symbol.species 기능

class Sports extends Array {};
const obj = new Sports(10, 20, 30);
const one = obj.slice(1, 2);
console.log(one);
// [20]
  1. class Sports extends Array {};
    Sports라는 클래스는 빌트인 Array오브젝트를 상속(확장, 연결)받습니다.
  2. const obj = new Sports(10, 20, 30);
    인스턴스를 생성합니다.
  3. const one = obj.slice(1, 2);
    obj 인스턴스의 slice()를 호출하면 인스턴스를 생성하고 메소드 처리결과를 인스턴스에 설정하여 인스턴스를 반환합니다.
  4. 이렇게 인스턴스의 메소드를 호출했을 때 인스턴스를 반환하도록 하는 것이 symbol.species의 기능입니다.

일반적으로 new 연산자로 클래스/생성자 함수를 호출하면 prototype에 연결된 constructor가 호출되고 인스턴스를 생성해서 반환합니다.

인스턴스의 메소드를 호출해서 반환하면 인스턴스가 반환되는데 obj는 인스턴스이기에 prototype이 없고 constructor가 없는데 어떻게 인스턴스를 만들수 있는가?

⇒ symbol.species가 constructor를 불러주는 역할을 수행합니다

symbol.species 오버라이드

  • 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를 사용할 수 있는 빌트인 오브젝트

    • Array, Map, Set, RegExp, Promise, ArrayBuffer, TypedArray
  • 빌트인 오브젝트를 상속받은 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
  1. class Sports extends Array{}
    빌트인 Array 오브젝트를 상속받습니다.
  2. static get [Symbol.species](){return Array;}
    빌트인 Array 오브젝트의 @@species를 오버라이드 합니다.
  3. console.log(one instanceof Sports);
    Sports로 one을 만들었으니 true입니다.
  4. const two = one.slice(1, 2);
    • Sports는 Array 오브젝트를 상속받았기에 one 인스턴스로 slice() 메소드 호출할 수 있습니다.
    • slice(1, 2) 대상은 인스턴스에 설정된 [10, 20 ,30] + 메소드 호출시 객체 생성시 준 값(10, 20, 30)을 가지고 slice(1, 2)를 실행하며 인스턴스를 생성하고 메소드 처리결과를 인스턴스에 설정하여 인스턴스를 반환합니다.
  5. Symbol.species로 오버라이드 했으므로
    • static get [Symbol.species](){}가 호출됩니다.
    • 호출에 사용한 one 인스턴스 형태를 반환하지 않고 Array 인스턴스를 반환합니다.
    • 이처럼 Symbol.species()로 반환할 인스턴스를 변경할 수 있습니다.

정리

  • 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로 생성한 인스턴스로 반환됩니다.


Symbol.toPrimitive

오브젝트를 대응하는 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]
// 책
  1. point.toString()
    point를 문자열로 변환하기위해 toString() 메소드를 호출하는데 Object.prototype.toString()이 호출되며 결과는 [object Object]입니다.
  2. `${book}`
    엔진이 오브젝트 내에서 toString()메소드를 찾습니다. book에는 toString()에 정의되어있기 떄문에 해당 함수가 호출되고 책 이 출력됩니다.

오브젝트를 숫자에 대응

const point = {bonus: 100};
console.log(point.valueOf());
const book = {
  toString() { return 70 },
  valueOf() { return 30 }
};
console.log(book * 20);
// {bonus: 100}
// 600
  1. point.valueOf()
    Object.prototype.valueOf()가 호출되어 {bonus: 100}이 출력됩니다
  2. book 20
    Object 타입
    Number 타입으로 성립될 수 없는 코드지만, 이는 Object타입이 Number타입의 Primitive 값으로 변환되기를 의도하는 코드이며 엔진에서는 오브젝트 내에서 valueOf() 메소드를 찾습니다. 만약 valueOf()가 없다면 toString()을 찾고 그 다음 Object의 prototype에 연결된 메소드를 호출합니다.

Symbol.toPrimitive() 사용

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는 해당 오브젝트를 호출하는 상황에 따라 맞게 들어옵니다.

hint의 타입

  • 곱셈(*)이나 나눗셈(/)등 수학적 연산처리의 경우 hint에 "number"
  • 템플릿(`${obj}`)은 hint에 "string"
  • 자료형이 ‘확실치 않을 때’, 동등 연산자 ==를 사용해 비교의 경우, 이항 덧셈 연산자 +(문자, 숫자 판별 어려움)에는 hint에 "default"

Symbol.iterator

  • @@iterator가 있는 빌트인 오브젝트
    • String, Array, Map, Set, TypedArray
    • 이터레이터 프로토콜에 따라 [Symbol.iterator]()로 이터레이터 오브젝트를 꺼낼 수도 있고 for-of문으로 쉽게 내부를 순회할 수도 있습니다.
  • 빌트인 Object에는 @@iterator가 없지만 개발자 코드로 작성할 수 있습니다.

Array.prototype[@@iterator]

  • Array 오브젝트의 [Symbol.iterator]()를 호출하면 이터레이터 오브젝트를 반환합니다.
  • next() 메소드를 호출해 배열 엘리먼트 값을 하나씩 구할 수 있습니다.
    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}

String.prototype[@@iterator]

  • String오브젝트의 [Symbol.iterator]()를 호출하면 이터레이터 오브젝트를 반환합니다.
  • next()메소드를 호출해 배열 엘리먼트 값을 하나씩 구할 수 있습니다.
    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}

Object 이터레이션

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
  • 빌트인 Object에는 Symbol.iterator가 없습니다.
  • Symbol iterator가 반복을 처리하므로 Object에 Symbol.iterator를 작성하면 반복이 가능해집니다.
  • 엔진이 for-of 문을 시작하면 먼저 obj에서 [Symbol.iterator]를 검색합니다. 이를 위해 obj에 [Symbol.iterator]를 작성합니다.
  • for (const value of obj)를 처음 실행할 때 obj의 [Symbol.iterator]()가 호출되며 return{}문을 수행합니다.
  • obj.maxCount = 2; 로 반복 횟수를 정의합니다.

[Symbol.iterator](){return {...}}
⇒ 빌트인 Object인 obj는 [Symbol.iterator]가 정의되어 있지 않기에 사용자 정의 이터레이터를 정의합니다.
⇒ 이터레이터 오브젝트를 사용할 때 호출하는 메소드인 next()를 구현합니다. 로직에 맞게 특정 조건까지 반환 오브젝트의 done을 false로 해 반복하게끔 하고, 반복이 종료되면 done:true를 반환해 이터레이터의 반복이 끝나도록 합니다.
⇒ maxCount를 설정해 maxCount까지 초기값이 0인 count가 도달할 때까지 순회하도록 반복하는 로직입니다.


Symbol.iterator에 제너레이터 함수 연결

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]
  1. obj[Symbol.iterator] = function*(){...}
    ⇒ obj의 Symbol.iterator에 제너레이터 함수를 연결했습니다.
  2. [...obj]
    ⇒ obj에서 [Symbol.itertor]를 검색합니다. 존재하므로 [Symbol.itertor]()를 실행하며 이터레이터 오브젝트를 생성해 반환합니다.
    ⇒ yield가 끝날 때까지(done:true) 반복하며 yield에서 반환된 값을 배열에 첨부합니다.
    ⇒ ...obj가 대괄호로 감싸져있지 않는다면 마지막 값(3)만 반환되어 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 처리를 공유한다는 점을 알 수 있습니다.


Symbol.match

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()
    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
    1. "/book/".startsWith(/book/);
      ⇒ 파라미터 /book/을 패턴으로 처리하면서 startsWith에서는 정규표현식을 사용할 수 없기에 에러가 발생합니다.
    2. check[Symbol.match] = false;
      ⇒ 파라미터를 정규표현식으로 인식하지 않도록 설정합니다.
    3. "/book/".startsWith(check);
      ⇒ 첫 번째와 동일하게 파라미터가 check의 값인"/book/"이지만 Symbol.match를 false로 설정해줬기에 check값을 문자열로 인식합니다. 그렇기 때문에 에러가 발생하지 않고 true가 출력됩니다.
profile
소신있게 정진합니다.

0개의 댓글