Object and Symbol

 

Created by 이항희 / blog.javarouka.me


본 자료는 ES2015 문법으로 작성되어 있습니다.

Object

JavaScript 의 Object 는 Key-Value Pair.

즉, Dictionary 나 Map 으로 불리는 것과 비슷하다.

게다가 JS 객체는 Object 와 모두 연결되어 있다


프로토타입 체인이라고 부른다

기본 메서드들

별도 구현하지 않을 경우 prototype chain 으로 Object 의 구현이 사용된다.

  • toString
  • valueOf
  • hasOwnProperty
  • isPrototypeOf

toString 메서드

객체의 문자열 표현을 반환

객체가 텍스트 혹은 문자열으로 표현될때 자동으로 호출된다.

const today = new Date();
const todayStrByOperator = today + " 입니다";
const todayStrByMethod = today.toString() + " 입니다";

console.log(todayStrByOperator === todayStrByMethod); // true

재미있는건 기본 toString, 즉 override 되지 않은 toString 은 반드시
'[object type]'

call 을 사용하여 context 를 변경 호출하면...

const object = {};
console.log(object.toString()) // [object Object]
console.log(Object.prototype.toString()) // [object Object]

const arr = [];
console.log(Object.prototype.toString.call(arr)) // [object Array]

const now = new Date();
console.log(Object.prototype.toString.call(now)) // [object Date]

이를 활용하여 뭔가 많이 부족한 typeof 를 대체할 타입 검사기를 만들 수도 있다

typeChecker.js
const typeChecker = ( _=> {
    const defaultToString = Object.prototype.toString; // 기본 객체의 toString 을 보관해둔다
    const types = [
        'Object','Array','Date','Boolean','Function','String','Number','RegExp','Null','Undefined'
    ];

    // reduce 함수를 사용하여 위에 정의한 타입으로 is + type 형식의 메서드를 생성한다.
    return types.reduce((prev, type) => {
        return prev[`is${type}`] = target => defaultToString.call(target) === `[object ${type}]`, prev
    }, {});
})();

console.log(typeChecker.isFunction(Date)); // true
console.log(typeChecker.isFunction(new Date)); // false
console.log(typeChecker.isDate(new Date())); // true
console.log(typeChecker.isUndefined(undefined)); // true

valueOf 메서드

객체의 기본값(primitive) 표현을 반환

명시적으로 호출할 일이 거의 없고, 대부분 엔진 내부에서 객체의 기본값 표현이 필요할 경우 자동으로 실행

toString 과 마찬가지로 대부분의 built-in Object 들은 이것을 override.

valueOf 구현 예제

function NiceNumber(primitiveValue) {
    this.primitiveValue = primitiveValue;
}

// @override
NiceNumber.prototype.valueOf = function() { return this.primitiveValue; }

const myNiceNumber = new NiceNumber(100);
console.log(myNiceNumber * 2) // 200
console.log(myNiceNumber.valueOf() * 2); // 200
console.log(myNiceNumber * myNiceNumber); // 10000
주의! Date 는 다르다!
const now = new Date();

// Date 구현의 경우 기본값 표현이 필요할 경우에도 toString 이 호출된다.
// 필요한 경우 타 객체와 달리 valueOf 명시적인 호출이 필요하다
const oneYearsAfter = new Date(now2.valueOf() + (1000 * 60 * 60 * 24 * 365)) // 1년 뒤

toJSON 메서드

객체의 JSON 표현을 반환

JSON.stringify 에 인자로서 호출될 경우 반환될 표현식을 정의할 수 있다

기본 동작
const sayMan = {
    hello: {
        'ko': '세계',
        'en': 'world'
    }
};
JSON.stringify(sayMan); // { "hello": { "ko": "세계", "en": "world" } }
toJSON 구현
const smartSayMan = {
    locale: 'ko',
    hello: {
        'ko': '세계',
        'en': 'world'
    },
    toJSON() {
        // global navigator 객체의 language 이용.
        return { hello: this.hello[navigator.language] }
    }
};
JSON.stringify(smartSayMan); // { "hello": "세계" } }

야, 할수 있어? 할 수 있냐고?

할수있지!

Duck Typing

저건 무엇인가?

난 무언가가 오리처럼 걷고 오리처럼 울어댄다면 그것을 오리라고 부르겠다
function cookDuckMeat(something) {

    // 오리처럼 꿕꿕대고, 걷고있나?
    if(!something.quack || !something.walk) {
        throw new TypeError('이건 오리가 아니다...');
    }

    // TODO: ... 오리 고기 요리 구현 ...
}
문자열이 필요할 때 만일 객체가 toString 을 할 수 있다면 그것을 호출해 주겠다.
const duck = {
   toString() { return "미운오리새끼" }
};

// toString 할 수 있어?
console.log(`저는 ${duck} 입니다`);

하지만 문자열로 행동을 체크하다니, 이거 위험한거 아닌가?

  • 문자열은 유니크한 값이 될 수 없다. 즉, 쉽게 덮어씌워질 수 있다.
  • 그 문자열을 다른 의미로 사용하고 있을 수 도 있다.

ES2015 에서는...

Symbol!

Symbol?

  • 일반적으로 생성할 경우 유일값을 가짐
  • 속성 키로 사용가능
  • 기본 타입임
// 객체의 키로 사용
const nameKey = Symbol('company');
const grandCompany = {
    [nameKey]: `대단한회사`
};
// const grandCompany = {};
// grandCompany[nameKey] = `대단한회사`;

// class 문법에도 사용가능
const quack = Symbol('duck.quack');
class Duck() {
    [quack]() { return "꽥!" }
}

const duck = new Duck()
duck[quack](); // "꽥!"
그렇지만, Reflection 에 다르게 대응함
const sym = Symbol('Symbol.key');
const prop = 'String.key'
const obj = { [sym]: '심볼 값', [prop]: '속성 값' }

for(var k in obj) {
console.log(k, obj[k]);
} // 'String.key 속성 값'

console.log(Object.keys(obj)) // ['String.key']

// Object.getOwnPropertySymbols 로 열거할 수 있다.
console.log(Object.getOwnPropertySymbols(obj)) // [Symbol(Symbol.key)]

고유성을 보장

// 생성시마다 유니크함
const s1 = Symbol();
const s2 = Symbol();
console.log(s1 === s2) // false;

const duck1 = Symbol('duck');
const duck2 = Symbol('duck');
console.log(duck1 === duck2) // false;
Symbol.for : 전역적으로 공유되는 심볼 생성
const unique = Symbol('unique'); // 그냥 생성
const uniqueFor1 = Symbol.for('unique'); // 전역 생성
const uniqueFor2 = Symbol.for('unique'); // 다시 생성할 경우 기존 생성된 Symbol 을 반환.
const uniqueFor2 = Symbol.keyFor('unique'); // 생성된 심볼을 가져올 경우 Symbol.for('unique') 와 동일 효과

console.log(unique === uniqueFor1); // false;
console.log(uniqueFor1 === uniqueFor2); // true;

Duck Typing 에 사용

네가 만약 Symbol.xxx 를 할수있다면 xxx 를 하겠다

const [ action,comedy,sing ] = ['action','comedy','sing'].map(v => Symbol(v));
function showVariety(star) {
    if(star[action]) star[action]();
    if(star[comedy]) star[comedy]();
    if(star[sing]) star[sing]();
}

var suhyungHong = {
    [action](){ return '연기를 합니다' }
}
console.log(showVariety(suhyungHong)); // 연기를 합니다

var changjungIm = {
    [sing](){ return '노래도 하고' },
    [action](){ return '연기도 하고' }
}
console.log(showVariety(changjungIm)); // 노래도 하고 연기도 하고

UUID 등 에 사용

불변 고유한 값이므로, 심볼을 공유하여 불변상태를 제어하기 쉽다

// 모델 코드. 변경 시마다 새로운 hash 를 저장한다.
user.update = function() {
    this.hash = Symbol(); // 업데이트 해쉬 발급
    // ... 모델 내 업데이트
}

// 화면 코드. 모델을 관리하는 store 등에서 변경 알림을 받는다고 가정한다.
userStore.subscribe = function(newUser) {
    if(newUser.hash == this.user) return; // 기존 user 와 비교하여 업데이트되지 않으면 pass
    // ... UI 업데이트
}

class private 구현에 사용

// @file Bart.js
const privateVars = Symbol('Bart.private');
export default class SecretDeveloper {
    constructor() {
        this[privateVars] = {
            name: '갓갓갓',
            desc: '비밀이 많다'
        }
    }
    get name() { return this[privateVars].name; }
    get desc() { return this[privateVars].desc; }
    toString() { return `${this.name}, ${this.desc}` }
}
const person = new SecretDeveloper();
console.log(`개발자 소개 > ${person}`); // DuckTyping 으로 toString 이 자동 호출
person.name = 'GodGodGod';
console.log(`수정된(?)  개발자 소개 > ${person}`); // immutable!

잘 알려진 Symbol (Well-Known Symbol)

앞서 설명한 toString, valueOf 등에 대응하는 심볼이 존재한다

참고: Well-known symbols

Symbol.toPrimitive

toString, valueOf 에 대응하지만 스펙이 좀 다르다.

const es6Man = {
    // 변환의 힌트가 인자로 온다. 힌트는 'string', 'number', 'default' 셋이다.
    [Symbol.toPrimitive](hint) {
        switch(hint) {
            case 'number': return 28;
            case 'string': return 'ES2015';
            default: return 'ES2015'
        }
    }
}
console.log(es6Man + 1); // ES20151
console.log(Number(es6Man)); // 28

console.log(es6Man); // ES2015
console.log(-es6Man); // 28
console.log(`${es6Man} 입니다`); // ES2015 입니다

Symbol.toStringTag

이 속성의 문자열은 Object.prototype.toString 의 컨텍스트로 호출할 경우 표현할 타입이 된다.

function Duck() {}
const duck = new Duck();
console.log(Object.prototype.toString.call(duck)); // [object Object]

function SmartDuck() {}
SmartDuck.prototype[Symbol.toStringTag] = "SmartDuck";
const startDuck = new SmartDuck();
console.log(Object.prototype.toString.call(startDuck)); // [object SmartDuck]

Symbol.hasInstance

생성자 객체가 어떤 객체를 자신의 인스턴스(instance)로 인식하는지 확인하는데 사용하는 메소드. instanceOf 연산 시 자동 호출된다.

const fakeDate = {
    [Symbol.hasInstance](instance) {
        return Date === instance.constructor;
    }
};

// Date 가 아닌데!!
console.log(new Date() instanceof fakeDate);
ES6은 오리가 필요한 모든곳에 Symbol 로 처리했을까?

아니다

Symbol 이라는게 있지만 아직 ES6의 몇몇 부분은 여전히 문자열을 사용한 DuckTyped 방식이다.

예) Promise.resolve의 then duck-typing
const thenable = {
    then(resolve, reject) {
        !!Math.round(new Date()%2) ? resolve('resolved!') : reject('rejected!')
    }
};

// 이것은 데너블(then-able). 문자열 then 의 속성으로 Promise.resolve 가 동작한다.
Promise.resolve(thenable)
    .then(fulfilled => {
        console.log(`WoW! ${fulfilled}`);
    })
    .then(rejected => {
        console.log(`Oops! ${rejected}`);
    })

하지만 신규로 제작된 스펙은 Symbol Duck Typed 이다.

Iterator #1
const arr = [1,2,3,4,5];
for(const el of arr) { console.log(el); }

const str = `hello`;
for(const el of str) { console.log(el); }

const mySet = new Set([1,2,3,4,5]);
for(const el of mySet) { console.log(el); }
아마 for - of 의 내부 해석은 이런 식일 것이다.
function forOf(iterator) {
    if(!(Symbol.iterator in iterator)) {
        throw new TypeError(`${iterator}[Symbol.iterator] is not a function`);
    }

    // 이터러블을 가져온다.
    // 이터러블은 next() 메소드를 가지며 { value: 순회값, done: 종료여부 } 를 반환하는 인터페이스를 말한다.
    // 문자열이 아닌 Symbol.next 의 방식이었다면 좋았을것 같지만 문자열 함수 방식이다.
    const iterable = iterator[Symbol.iterator]();

    if(typeof iterable.next !== 'function') {
        throw new TypeError(`iterator.next is not a function(…)`)
    }

    // ...순회 구현... for...while..
}

이 외의 Well-Known Symbol 은 다음 링크에.

Well-known symbols

참고자료