Skip to content

Latest commit

 

History

History
529 lines (414 loc) · 16.9 KB

ch6.Symbol.Taek.md

File metadata and controls

529 lines (414 loc) · 16.9 KB

6장 심벌과 심벌 프로퍼티

primitive value: string, number, boolean, null, undefined

# es6
symbol
- 새로운 원시 타입
- 비공개 객체 멤버를 만들기 위한 한 가지 방법으로 출발했다.
- 기존: 문자열 이름을 가진 모든 프로퍼티에 그 이름을 알고 있는지와 상관없이 쉽게 접근할 수 있었다.
- '비공개 이름' -> 개발자가 문자열이 아닌 프로퍼티 이름을 만들 수 있게 됨 -> 일반적인 방식으로는 비공개 이름을 알아낼 수 없다.
- 이런 개념이 es6에서 symbol로 발전

- 심벌을 통해 문자열이 아닌 프로퍼티 이름을 추가했지만, 외부 접근 방지를 위한 목표는 제외
- 심벌 프로퍼티는 다른 객체 프로퍼티와는 별개로 분류된다.

boolean의 true나 숫자의 30처럼 문자 리터럴 형태가 없다.

var person = {};
person["firstName"] = "victor";

let firstName = Symbol();
let person = {};
person[firstName] = "victor";
console.log(person[firstName]); // victor


// 심벌의 서술 문자열
// 심벌을 쉽게 읽고 디버깅할 수 있도록 항상 제공하는 것이 좋다.
firstName = Symbol("first name");
console.log("first name" in person); // false
console.log(person[firstName]); // "victor"
console.log(firstName); // "Symbol(first name)"

// typeof 사용
console.log(typeof firstName); // "symbol"

장점:

  • 프로퍼티를 할당할 때 심벌을 사용하면, 같은 프로퍼티에 접근하려 할 때마다 그 심벌을 사용해야 한다.

특징:

  • 심벌의 서술 문자열은 [[Description]] 프로퍼티에 저장되고 심벌의 toString() 메소드가 호출될 때 사용

Object.defineProperty() Object.defineProperties() 계산된 객체 리터럴 프로퍼티 이름

let firstName = Symbol("first name");

// 계산된 객체 리터럴 프로퍼티에 사용
let person = {
  [firstName]: "victor"
};

// 프로퍼티를 읽기 전용으로 만듦
Object.defineProperty(person, firstName, {writable: false});

let lastName = Symbol("last name");
Object.defineProperties(person, {
  [lastName]: {
    value: "Lim",
    writable: false
  }

});
console.log(person[firstName]); // "victor"
console.log(person[lastname]); // "Lim"
console.log(Reflect.ownKeys(person));  // ['key', 'method', Symbol(symbol), Symbol(symbol2)]

전역 심벌 저장소:

  • 심벌을 저장해 놓고 검색해서 사용
  • Symbol.for(String 심벌 서술 문자열): 있으면 새로운 해당 심벌을 검색해서 가져오고 없으면 새로운 심벌을 만든다
const uid = Symbol.for("uid 111"); // 새로운 심벌
let obj = {
  [uid]: "12345"
};

console.log(obj[uid]); // "12345"
console.log(uid); // "Symbol(uid)"

let uid2 = Symbol.for("uid 111"); // 기존 uid 심벌~ from global symbol storage
console.log(uid === uid2); // "12345"
console.log(obj[uid2]); // "12345"
console.log(uid2); // "Symbol(uid)"

Symbol.keyFor(String 심벌 서술 문자열): 전역 심벌 저장소에서 해당 심벌 서술 문자열을 가진 심벌과 관련된 키를 찾을 수 있다.

const uid = Symbol.for("uid 111");
console.log(Symbol.keyFor(uid)); // "uid"

const uid2 = Symbol.for("uid 111");
console.log(Symbol.keyFor(uid2)); // "uid"

const uid3 = Symbol("uid 111"); // 심벌 전역 저장소 사용x
console.log(Symbol.keyFor(uid3)); // undefined

주의사항: 전역 심벌 저장소는 전역 스코프처럼 공유된 환경이다. 이는 그 환경에 키가 이미 존재하는지 아닌지에 대해 추정할 수 없다는 의미이다. 써드파티 컴포넌트를 사용할 때 이름 충돌의 가능성을 줄이기 위해서는 심벌 키에 네임스페이스를 사용해야 한다.

const isArray = Symbol('isArray');
const isRegExp = Symbol('isArray');
const isDate = Symbol('isArray');
const isObject = Symbol('isArray');
Array[isArray] = arg => {
  // 코드 작성
};
RegExp[isRegExp] = arg => {
  // 코드 작성
};
Date[isDate] = arg => {
  // 코드 작성
};
Object[isObject] = arg => {
  // 코드 작성
};

전역공간에 노출 시키기 싫을 때!

const $SYMBOL = { // 전역 공간에 노출되므로 그 어떤 이름과도 충돌이 나지 않게 지어야한다.
  isArray: Symbol('isArray'),
  isRegExp: Symbol('isArray'),
  isDate: Symbol('isArray'),
  isObject: Symbol('isArray')
};
Array[$SYMBOL.isArray] = arg => {
  // 코드 작성
};
RegExp[$SYMBOL.isRegExp] = arg => {
  // 코드 작성
};
Date[$SYMBOL.isDate] = arg => {
  // 코드 작성
};
Object[$SYMBOL.isObject] = arg => {
  // 코드 작성
};

reference: Symbol의 사용 사례

  • 심벌은 타입 변환에 유연하지 않다. why? 다른 타입들과 논리적 통치가 성립 X
let uid = Symbol.for("uid");
let desc = String(uid); // "Symbol(uid)"

desc = uid + ""; // 에러
desc = uid / 1; // 에러

if (uid) {
  // 실행~
};

Object.keys(): 열거 가능한 모든 프로퍼티 이름을 반환 Object.getOwnPropertyNames(): 열거 가능 여부와 상관없이 모든 프로퍼티 이름 반환

, but 심벌 프로퍼티 반환 x (es5와 호환성 문제)

Object.getOwnPropertySymbols(): 객체가 소유한 프로퍼티 심벌을 반환

let uid = Symbol.for("uid victor");
let obj = {
  [uid]: "lim~~~",
  nick: "dio",
  age: 34,
  get age() {
    return this.age;
  },
  sayHello() {
    console.log(this.nick + " is saying hello!");
  },
  "job": "developer"
};

let keys = Object.keys(obj);
let values = Object.values(obj);
let propertyNames = Object.getOwnPropertySymbos(obj);
let propertySymbols = Object.getOwnPropertySymbols(obj);

console.log(keys);
console.log(values);
console.log(propertyNames);
console.log(propertySymbols);

es5: 자바 스크립트의 '마법 같은'부분 일부를 노출하고 정의하는 것 es6: 특정 객체의 기본 동작 정의에 심벌 프로토타입 프로퍼티를 사용하여, 이전에는 언어의 내부 로직이었던 부분을 훨씬 더 많이 노출하는 방식으로 전통을 이어나감

  • 결과: 내부 전용 연산자로 여겨지던 공통 동작을 상용 심벌(well-known symbols)로 미리 정의
    • 상용 심벌: Symbol.match처럼 Symbol 객체의 프로퍼티로 나타냄
      • Symbol.hasInstance
      • Symbol.isConcatSpreadable
      • Symbol.iterator
      • Symbol.match
      • Symbol.replace
      • Symbol.search
      • Symbol.species
      • Symbol.split
      • Symbol.toPrimitive
      • Symbol.toStringTag
      • Symbol.unscopables

instanceof: ArraySymbol.hasInstance >> 동일

obj instanceof Array

Array[Symbol.hasInstance](obj)
  • es6에서는 기본적으로 instanceof 연산자가 이 메서드 호출의 단축 문법으로 재정의
  • instanceof가 메서드 호출과 연결되었으므로 instanceof의 동작을 변경할 수 있다!!!!
function MyObject() {
  // ...
}

Object.defineProperty(MyObject, Symbol.hasInstance, {
  value: function(v) {
    return false;
  }
});

let obj = new MyObject();
console.log(obj instanceof MyObject);
  • 쓰기 불가능한 프로퍼티를 덮어쓰기 위해서는 Object.defineProperty() >> 해보기
function SpecialNumber() {
  // 비어 있음
}

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
let colors1 = ["red", "green"];
let colors2 = colors1.concat(["blue", "black"]);

console.log(colors.length); // 4
console.log(colors2); // ["red", "green", "blue", "black"]

// 배열이 아닌 인자도 받을 수 있다! 끝에 추가
colors2 = colors1.concat(["blue", "black"], "brown");
console.log(colors2.length); // 5
console.log(colors2); // ["red", "green", "blue", "black", "brown"]
  • 어떤 타입이든 concat() 호출 시 배열처럼 동작하도록 정의할 수 있다.
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 프로퍼티 두 숫자 키를 갖도록 하여 배열로 보이게 설정
    • [Symbol.isConcatSpreadable]: true >> 배열에 개별 요소로 추가 되도록 했다.
  • 문자열 타입은 정규표현식을 인자로 받는 몇 가지 메서드를 가지고 있다.
    • match(regex)
    • replace(regex, replacement)
    • search(regex)
    • split(regex) 이 네 가지 메서드에 해당하는 심벌들을 정의하고, 고유 동작을 내장 객체인 RegExp에 효과적으로 위탁한다

Symbol.match, Symbol.replace, Symbol.search, Symbol.split 심벌은 각각

  • match(regex)
  • replace(regex, replacement)
  • search(regex)
  • split(regex) 위 메서드의 첫 번째 인자에 전달되는 정규 표현식 인자의 메서드를 나타낸다. 네 가지 심벌 프로퍼티는 문자열 메서드가 사용해야 하는 기본 구현으로, RegExp.prototype에 정의 된다.
Symbol.match: 인자 받고 매칭되는 배열 반환, 없으면 null
Symbol.replace: 문자열 인자와 대체 문자열을 받고 대체된 문자열 반환
Symbol.search: 문자열 인자를 받아 매칭된 숫자 인덱스를 반환, 없으면 -1
Symbol.split: 문자열 인자를 받아 매칭되는 문자열로 나눠진 문자열 배열을 반환
// 사실상 /^.{10}$/와 같다. >> .에 해당하는 10개로만 이루어진 문자열
let hasLengthOf10 = {
    [Symbol.match]: function(value) {
      return value.length === 10 ? [value] : null;
    },
    [Symbol.replace]: function (value) {
      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];
    }
};
  • 자바스크립트에서는 종종 특정 연산이 적용될 때 암묵적으로 객체를 원시값으로 변환하려고 시도한다. (e.g. ==)

  • Symbol.toPrimitive를 통해서 제어 가능

    • Symbol.toPrimitive 메서드는 각 표준 타입의 포로토타입에 정의되고 객체가 원시값으로 변환될 때 무엇을 해야 할지 규정한다.
    • 원시값 변환이 필요할 때 명세에서 hint로 불리는 인자와 함께 호출
      • hint
        • string
        • Number
        • default
  • 숫자 모드

    • valueof() 메서드를 호출하고, 그 결과가 원시 값이면 반환한다.
    • 그렇지 않으면, toString() 메서드를 호출하고, 그 결과가 원시 값이면 반환한다.
    • 그렇지 않으면, 에러
  • 문자열 모드

    • toString() 메서드를 호출하고, 그 결과가 원시 값이면 반환한다.
    • 그렇지 않으면, valueOf() 메서드를 호출하고, 그 결과가 원시 값이면 반환
    • 아니면 에러
  • 기본 모드

    • 많은 경우 숫자 모드와 동일하게 처리
    • Date >> 문자열 모드
    • ==, +, Date 생성자에 인자 하나를 전달하는 경우에만 사용된다.
    • Symbol.toPrimitive 메서드를 정의하여, 이러한 기본 타입 변환 동작을 오버라이드 할 수 있다.
function Temperature(degrees) {
  this.degrees = degrees;
}

Temperature.prototype[Symbol.toPrimitive] = function(hint) {

  switch (hint) {
    case "string":
      return this.degrees + "\u00b0"; // 온도 기호
    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'"
  • 다양한 전역 실행환경 존재
    • 데이터 전달 문제 x
    • 객체가 다른 객체들 사이에서 전달된 후에 처리하려는 객체의 타입을 식별하려 할 때 발생
      • iframe to 부모 or vice versa
        • why? 영역(Realm)을 각각 다르게 나타낸다.
        • 각 영역은 전역 객체의 복사본을 소유한 전역 스코프를 가진다.
          • 배열이 어느 영역에서 만들어졌든지, 그것은 분명히 하나의 배열이다.
            • 그러나 한 영역에서 만들어진 배열이 다른 영역에 전달되었을 때 instanceof Array를 호출하면 false 반환
              • why???????????? >> 배열이 다른 영역의 생성자로 만들어졌고 Array는 현재 영역의 생성자를 나타내기 때문이다

식별 문제를 우회하는 방법

  • 기존: toString() 호출하면 예측 가능한 문자열 반환
function isArray(value) {
  return Object.prototype.toString.call(value) === "[object Array]";
}
console.log(isArray([])); // true
  • 번역을 이상하게 한듯...

  • es5 이전

    • JSON 전역 객체를 만들 때 더글러스 크락포드(Douglas Crockford)의 json2.js 사용
// 전역 객체가 자바 스크립트 환경에서 제공되는지 아니면 다른 라이브러리를 통해서 제공되는지 파악하는 것이 필수적이게 되었다.
// isArray() 방식으로 파악하는 함수를 사용하게 됨
function supportsNativeJSON() {
  return typeof JSON !== "undefined" && Object.prototype.toString.call(JSON) === "[Object JSON]";
}
  • native: [Object JSON]
  • 아닐 때: [Object object]

####ECMAScript 6의 객체 문자열 태그 정의

  • Symbol.toStringTag 심벌을 통해 위에서 사용한 toString()을 재정의 가능
function Person(name) {
  this.name = name;
}

Person.prototype[Symbol.toStringTag] = "Person";
let me = new Person("victor");
console.log(me.toString()); // "[object Person]"
console.log(Object.prototype.toString.call(me)); // "[object Person]"

Person.prototype[Symbol.toStringTag] = "Array";

Person.prototype.toString = function() {
  return this.name;
};

me = new Person("victor");

console.log(me.toString()); // "victor"
console.log(Object.prototype.toString.call(me)); // "[object Array]"

Array.prototype[Symbol.toStringTag] = "Magic";
let values = [];

console.log(Object.prototype.toString.call(values)); // "[object Magic]"

신기하네~~~~

  • with 문: 반복을 피하려는데.. 복잡해... // strict 모드 사용 불가
var values = [1, 2, 3];
var colors = ["red", "green", "blue"];
var color = "black";

with(colors) {
  push(color);
  push(...values);
}

console.log(colors); // ["red", "green", "blue", "black", 1, 2, 3]
  • with 문: push를 지역 바인딩 >>> colors

  • es6에서 Array에 values 메소드 추가 >> 결과적으로 with 문 내에서 ...values는 밖의 values가 아닌 배열의 values 메서드 참조

  • 해결: Symbol.unscopables: with문 내 어떤 프로퍼티 바인딩을 만들면 안 되는지 명시하기 위해 Array.prototype 에서 사용

Array.prototype[Symbo.unscopables] = Object.assign(Object.create(null), {
  copyWithin: true,
  entries: true,
  fill: true,
  find: true,
  findIndex: true,
  keys: true,
  values: true
});
  • 이 메서드의 바인딩은 with문 내에 만들어지지 않고, 아무런 문제 없이 오래된 코드에서 잘 동작하도록 해준다.
  • 새로운 원시값
  • 심벌 참조 없이 접근할 수 없는 열거 불가능 프로퍼티를 만드는데 사용
  • 완벽한 비공개 x, but 모든 개발자로부터 보호할 필요가 있는 기능에 적합하다.
  • 심벌 서술 문자열
  • 전역 심벌 저장소
  • Object.getOwnPropertySymbols()
  • Object.defineProperty, Object.defineProperties를 호출 해 여전히 심벌 프로퍼티를 변경할 수 있다.
  • Symbol.을 사용해 표준 객체의 기능을 다양한 방법으로 수정할 수 있게 됨