심볼형 symbol

김수정·2020년 4월 9일
0
post-custom-banner

Symbol?

객체의 비공개 멤버에 대한 니즈에서 탄생한 원시형 데이터 입니다.

Symbol([symbolName])
() 안에는 심볼 이름을 담을 수 있고, 같은 이름으로 만들어도 각기 다른 심볼입니다. (symbolName)은 오직 이름표 역할만 하며, 문자열이 아닌 타입이 오면 자동으로 toString 처리합니다.
이름만 출력하고 싶다면 심볼변수.description를 사용합니다.

let id1 = Symbol("id");
let id2 = Symbol("id");

alert(id1 == id2); // false

Symbol 특징

원시형 데이터로 유일무이하고 고유한 존재

let id1 = Symbol("id");
let id2 = Symbol("id");

alert(id1 == id2); // false
alert(id1 === id2); // false

객체의 프로퍼티 키

심볼과 문자형은 객체의 프로퍼티 키가 될 수 있는 특별한 자료형입니다.
그런데 심볼은 문자형과 사용법이 좀 다릅니다.
Symbol은 식으로 생성되어야만 쓸 수 있어서 바로 객체의 프로퍼터 키 자리에 심볼 식을 쓸 수는 없고 대괄호 표기법으로 값을 넣어줘야 합니다.
변수에 담아서 넣어줘야만 나중에 외부에서 접근하고 싶을 때 그 변수를 리턴해주면 되는데 대괄호 표기법 안에 바로 심볼 식을 넣어버리면 나중에 본인도 접근할 수가 없습니다.

const NAME = Symbol('이름')
const GENDER = Symbol('성별')
const iu = {
  [NAME]: '아이유',
  [GENDER]: 'female',
  age: 26
}
const suzi = {
  [NAME]: '수지',
  [GENDER]: 'female',
  age: 26
}
const jn = {
  [NAME]: '재남',
  [GENDER]: 'male',
  age: 30
}

console.log(iu, suzi, jn)

기본적인 열거 대상에서 제외

아래 두 경우에서는 가능은 하지만 번거롭고 정상적인 접근이라고 보기는 힘듭니다.
X for..in
X Object.keys(obj)
X Object.getOwnPropertyNames(obj)
Q Object.getOwnPropertySymbols(obj) - 심볼만 열거됨
O Reflect.ownKeys(obj)

console.log(iu[NAME], suzi[NAME], jn[NAME])

for (const prop in iu) {
  console.log(prop, iu[prop])
}

Object.keys(iu).forEach(k => {
  console.log(k, iu[k])
})

Object.getOwnPropertyNames(iu).forEach(k => {
  console.log(k, iu[k])
})

Object.getOwnPropertySymbols(iu).forEach(k => {
  console.log(k, iu[k])
}) // 심볼만 열거됨

Reflect.ownKeys(iu).forEach(k => {
  console.log(k, iu[k])
}) // 다 열거됨

암묵적 형변환 불가능

let id1 = Symbol("id");
console.log(id1 + 1); // error

hidden property

객체지향 방식에서 사용하는 private 변수를 흉내낸 것입니다. 즉, 따로 설정을 해주지 않으면 외부에서 접근이 불가능하고 수정할 수도 없는 프로퍼티입니다. 외부 스크립트나 라이브러리에 속한 객체에 새로운 프로퍼티를 추가할 때도 사용할 수 있습니다.

  • 그러나 Object.assign은 심볼도 복사가 됩니다.
const x = () => {
  const a = Symbol('a');
  return {
    [a]: 10,
    a: a, // 외부에서 사용할 수 있도록 리턴해 줌.
  }
};
const y = x();
y.a // undefined
y[a] // undefined
y[Symbol('a')] .. // undefined
y[y.a] // 10

타입 체크

const sb = Symbol(null)
console.log(typeof sb) // symbol

공유 심볼(전역 심볼)

원래 심볼이름이 같아도 다른 심볼로 취급되지만, 같은 이름으로 동일한 심볼에 접근하고 싶을 경우를 대비해 전역 심볼을 만들어두었습니다.
Symbol.for(name)은 전역 공간에 접근할 수 있습니다.
Symbol.keyFor(sym)은 공유심볼 이름을 얻을 수 있습니다. 일반 심볼 이름을 얻고 싶다면 symbolName.description을 사용합니다.

let id = Symbol.for("id"); // 심볼이 존재하지 않으면 새로운 심볼을 만듭니다.

// 동일한 이름을 이용해 심볼을 다시 읽습니다(좀 더 멀리 떨어진 코드에서도 가능합니다).
let idAgain = Symbol.for("id");

// 두 심볼은 같습니다.
alert( id === idAgain ); // true

위와 같이 심볼 이름으로 동일성을 확보할 수 있습니다. 따라서 원래 심볼에서 불가능했던 함수 안에서 만들어진 심볼 접근도 가능해 집니다.

const obj = (() => {
  const COMMON1 = Symbol.for('공유심볼')
  return {
    [COMMON1]: '공유할 프로퍼티 키값이에요. 어디서든 접근 가능하답니다.'
  }
})()
const COMMON2 = Symbol.for('공유심볼')
console.log(obj[COMMON2]) // '공유할 프로퍼티 키값이에요. 어디서든 접근 가능하답니다.'

시스템 심볼(표준 심볼)

심볼을 재정의함으로써 기존에는 표준객체 내부 전용이던 기능들을 개발자의 입맛에 맞게 바꾸어 쓸 수 있게 만든 것입니다.

Symbol.hasInstance

instance instanceof constructor 명령은 내부적으로 constructor[Symbol.hasInstance](instance) 으로 동작.

class Animal {
  static [Symbol.hasInstance](obj) {
    if (obj.canEat) return true;
  }
}

let obj = { canEat: true };

alert(obj instanceof Animal); // true, Animal[Symbol.hasInstance](obj)가 호출됨

Symbol.isConcatSpreadable

배열 concat메서드에서 배열을 합치고자 할 때, 이 객체의 프로퍼티들을 배열의 값으로 넘겨줄 수 있습니다.

let arr = [1, 2];

let arrayLike = {
  0: "something",
  1: "else",
  [Symbol.isConcatSpreadable]: true,
  length: 2
};

alert( arr.concat(arrayLike) ); // 1,2,something,else

Symbol.iterator

이터러블 객체로 만들어 줍니다.
Symbol.iterator는 iterator를 반환합니다.
iterator는 next메서드를 갖고 있는 객체입니다.
next 메서드는 {done: Boolean, value: any} 형태를 리턴합니다. done이 false일 때는 value에 다음 값이 저장됩니다.

단점은 두 개의 for..of문을 하나의 객체에 동시에 사용할 수 없습니다. 이터레이터가 하나 뿐이어서 두 반복문의 반복 상태를 공유하기 때문입니다.

let range = {
  from: 1,
  to: 5
};

// 1. for..of 최초 호출 시, Symbol.iterator가 호출됩니다.
range[Symbol.iterator] = function() {

  // Symbol.iterator는 이터레이터 객체를 반환합니다.
  // 2. 이후 for..of는 반환된 이터레이터 객체만을 대상으로 동작하는데, 이때 다음 값도 정해집니다.
  return {
    current: this.from,
    last: this.to,

    // 3. for..of 반복문에 의해 반복마다 next()가 호출됩니다.
    next() {
      // 4. next()는 값을 객체 {done:.., value :...}형태로 반환해야 합니다.
      if (this.current <= this.last) {
        return { done: false, value: this.current++ };
      } else {
        return { done: true };
      }
    }
  };
};

// 이제 의도한 대로 동작합니다!
for (let num of range) {
  alert(num); // 1, then 2, 3, 4, 5
}

이터레이터를 명시적으로 호출하기

et str = "Hello";

// for..of를 사용한 것과 동일한 작업을 합니다.
// for (let char of str) alert(char);

let iterator = str[Symbol.iterator]();

while (true) {
  let result = iterator.next();
  if (result.done) break;
  alert(result.value); // 글자가 하나씩 출력됩니다.
}

Symbol.toPrimitive

해당 객체의 원시형변환을 정의합니다. 로깅이나 디버깅 용도로 좋습니다.
예를 들어 우리가 디버깅을 위해 어떤 변수를 콘솔에 찍어보았습니다. 그런데 객체라면 맨날 [object object]로 나와 인상을 찌뿌리게 만들었죠. 그래서 이게 뭐라는건지.. Date객체라는 건지 내가 정의한 객체라는 건지 뭔지!!
저런 현상은 객체가 문자형으로 형변환 되면서 저렇게 변한 것인데요. 이제 심볼을 이용해서 이 걸 우리가 원하는 값을 반환하도록 설정할 수 있습니다!

Symbol.toPrimitive는 반드시 원시값을 반환해야 하며 hint는 string, number, default 중에 하나입니다.
원시 값이 아니라면 에러가 발생하고 형변환을 한 곳에서 정의할 수 있는 점이 장점입니다. 기존에 이를 대체하던 메서드는 toString()과 valueOf()가 있습니다.

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

toString && valueOf
이전부터 있던 메서드입니다. toString은 우리가 객체를 콘솔에 찍었을 때 많이 봤던.. 아무 도움 안되는 [object object]를 반환합니다 ^_^
valueOf는 객체 자신을 반환합니다. 이전 환경에서 작업해야 하고 심볼을 사용할 수 없다면 toString과 valueof를 객체에서 정의하여 심볼과 동일하게 동작하도록 구현할 수도 있습니다. 그렇지만 이들은 원시 값이 아닌 객체 자체가 반환되어도 에러를 발생하지 않으므로 심볼을 사용하는 것이 좋습니다. Symbol.toPrimitive와 valueOf가 없으면 toString이 모든 형변환을 처리합니다.

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

혹은 Symbol.toStringTag를 사용해 toString에 대한 부분을 정의할 수도 있습니다.

class Person {
  constructor (name) { this.name = name }
}
const jn = new Person('susu')
console.log(jn.toString())

Person.prototype[Symbol.toStringTag] = 'PERSON'
console.log(jn.toString()) // object susu

기타

  • Symbol.match: 정규표현식 및 문자열 관련.
  • Symbol.replace: 정규표현식 및 문자열 관련.
  • Symbol.search: 정규표현식 및 문자열 관련.
  • Symbol.species: 파생클래스를 만들기 위한 생성자.
  • Symbol.split: 원래 문자열을 나누는 메소드였던 split의 동작방식을 재정의.
  • Symbol.unscopables: with문과 관련. 스코프 영향 받게 할지의 여부.
profile
정리하는 개발자
post-custom-banner

0개의 댓글