프로토타입 메서드와 __proto__가 없는 객체

이 절의 첫 번째 챕터에서 프로토타입을 설정하기 위한 모던한 방법이 있다고 언급했습니다.

__proto__는 다소 구식이고 더는 사용하지 말아야 할 것으로 봅니다(자바스크립트 표준 중 브라우저 부분에서).

모던한 방식은 다음과 같습니다.

__proto__ 대신 이 메서드들을 사용하는 것이 좋습니다.

예시:

let animal = {
  eats: true
};

// 프로토타입이 animal인 새로운 객체를 생성합니다.
let rabbit = Object.create(animal);

alert(rabbit.eats); // true

alert(Object.getPrototypeOf(rabbit) === animal); // true

Object.setPrototypeOf(rabbit, {}); // rabbit의 프로토타입을 {}으로 바꿉니다.

Object.create에 프로퍼티 설명자를 선택적으로 전달할 수도 있습니다. 다음과 같이 설명자를 이용해 새로운 객체에 프로퍼티를 추가할 수 있습니다.

let animal = {
  eats: true
};

let rabbit = Object.create(animal, {
  jumps: {
    value: true
  }
});

alert(rabbit.jumps); // true

설명자는 프로퍼티 플래그와 설명자에서 설명하는 것과 같은 형태로 사용합니다.

Object.create를 사용하면 for..in을 사용해 프로퍼티를 복사할 때보다 더 효과적으로 객체를 복제할 수 있습니다.

// obj와 완벽하게 동일한 얕은 사본
let clone = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));

이 호출은 obj의 모든 프로퍼티를 포함한 완벽한 사본을 만들어냅니다. 사본은 열거 가능한 프로퍼티와 불가능한 프로퍼티, 데이터 프로퍼티, getter, setter 등 모든 프로퍼티와 올바른 [[Prototype]]을 가지게 됩니다.

간략한 역사

[[Prototype]]을 다루기 위한 방법은 아주 많습니다! 같은 것을 하기 위한 여러 방법이 있는 것입니다!

왜 그럴까요?

역사적인 이유가 있습니다.

  • 생성자의 "prototype" 프로퍼티가 아주 오래전부터 있었습니다.
  • 후에 2012년에 이르러 Object.create가 표준에 등장했습니다. 이를 이용해 주어진 프로토타입을 가진 객체를 만들 수는 있었지만, 프로토타입을 get/set할 수는 없었습니다. 그래서 언제든지 프로토타입을 get/set 할 수 있도록 비표준 __proto__ 접근자가 브라우저에 구현되었습니다.
  • 2015년에는 Object.setPrototypeOfObject.getPrototypeOf가 표준에 추가되었고 __proto__와 동일한 기능을 수행하게 되었습니다. 사실상 __proto__가 모든 곳에 구현되어있었기 때문에, __proto__는 앞으로 더 이상 사용되지 말하야 할 것으로 보고 표준의 부록 B(Annex B)에 추가되었습니다. 브라우저가 아닌 환경에서 __proto__는 선택사항입니다.

오늘날에는 이 모든 방식을 사용할 수 있습니다.

__proto__를 함수 getPrototypeOf/setPrototypeOf로 대체했을까요? 흥미로운 질문입니다. 이 질문에 대한 답은 __proto__가 왜 나쁜지 이해하면 얻을 수 있습니다. 답을 얻기 위해 계속 읽어 봅시다.

속도가 중요하다면 이미 존재하는 객체의 [[Prototype]]을 바꾸지 마세요.

기술적으로는 [[Prototype]]을 언제든지 get/set할 수 있습니다. 하지만 일반적으로 생성 시점에만 [[Prototype]]을 설정하고 이후엔 수정하지 않습니다. rabbitanimal을 상속하고 그 사실은 변하지 않습니다.

자바스크립트 엔진은 이를 토대로 최적화되어 있습니다. Object.setPrototypeOfobj.__proto__=를 써서 프로토타입을 그때그때 바꾸는 것은 매우 느립니다. 객체의 프로퍼티에 접근하는 동작의 최적화를 깨기 때문입니다. 그러므로 [[Prototype]]을 바꾸는 것이 어떤 결과를 초래할지 확실히 알거나 자바스크립트의 속도가 전혀 중요하지 않은 경우가 아니라면 [[Prototype]]을 바꾸지 마세요.

'매우 단순한' 객체

알다시피 객체는 키/값 쌍을 저장할 수 있는 연관 배열입니다.

하지만 사용자가 제공한 키를 저장하려고 할 때(예를 들어 사용자가 사전을 만들 때), 사소한 결함을 하나 볼 수 있습니다. 다른 모든 문자열은 키로 사용할 수 있지만 "__proto__"는 사용할 수 없습니다.

다음 예제를 살펴봅시다.

let obj = {};

let key = prompt("What's the key?", "__proto__");
obj[key] = "some value";

alert(obj[key]); // "some value"가 아니라 [object Object]입니다!

여기서 사용자가 __proto__를 입력하면 할당이 되지 않습니다.

사실 놀랄 일은 아닙니다. __proto__ 프로퍼티는 특별하기 때문입니다. __proto__는 항상 객체이거나 null이어야 합니다. 문자열 타입은 프로토타입으로 사용할 수 없습니다.

하지만 의도치 않게 그런 식으로 구현할 수 있습니다. 그렇죠? 키/값 쌍을 저장하려는데 키의 이름이 "__proto__"이면 제대로 저장되지 않습니다. 그러니 이건 버그가 맞습니다.

여기선 그 결과가 그리 치명적이진 않습니다. 그렇지만 객체를 값으로 할당하는 경우엔 프로토타입이 정말 바뀔 수 있습니다. 그렇게 되면 완전히 기대하지 않은 것이 실행될 수 있습니다.

설상가상으로 개발자들은 대게 프로토타입이 바뀔 가능성을 전혀 생각하지 않습니다. 그래서 프로토타입이 바뀐 것은 눈치채기도 힘들고, 서버 사이드에서 자바스크립트를 사용중일 땐 심지어 취약점이 되기도 합니다.

이런 기대하지 않은 동작은 원래 함수인 toString이나 다른 내장 메서드에 할당을 할 때도 일어날 수 있습니다.

이 문제를 어떻게 피할 수 있을까요?

우선 을 사용하면 모든 것이 해결됩니다.

하지만 객체를 써도 괜찮습니다. 언어를 만든 사람들이 오래전에 이 문제를 고려했기 때문입니다.

__proto__는 객체의 프로퍼티가 아니라 Object.prototype의 접근자입니다.

그래서 obj.__proto__를 읽거나 쓸 때, 이에 대응하는 getter/setter를 프로토타입에서 호출해 [[Prototype]]을 가져오거나 설정합니다.

이 절을 시작할 때 언급한것 처럼 __proto__[[Prototype]]에 접근하기 위한 방법이지 [[Prototype]] 그 자체가 아닙니다.

자 이제 객체를 연관 배열로 사용하고 싶다면 간단한 트릭을 사용하면 됩니다.

let obj = Object.create(null);

let key = prompt("What's the key?", "__proto__");
obj[key] = "some value";

alert(obj[key]); // "some value"

Object.create(null)은 프로토타입이 없는 빈 객체를 생성합니다([[Prototype]]null).

그러므로 __proto__ getter/setter를 상속하지 않습니다. 이제 "__proto__"는 일반적인 데이터 프로퍼티로 처리되므로 위의 예제는 잘 동작하게 됩니다.

이런 객체를 ‘매우 단순한(very plain)’ 객체 혹은 '순수 사전식 객체(pure dictionary objects)'라고 부릅니다. 일반적으로 많이 사용하는 단순한 객체 {...} 보다 훨씬 단순하기 때문이죠.

이 방식의 단점은 이런 객체들은 내장 객체 메서드가 없다는 것입니다. toString을 예로 들 수 있습니다.

let obj = Object.create(null);

alert(obj); // Error (toString이 없음)

하지만 연관 배열이니 보통은 괜찮습니다.

객체와 연관된 메서드 대부분은 Object.keys(obj)와 같이 Object.something(...)형태라는 점을 유의하세요. 프로토타입에 있는 게 아니기 때문에 '매우 단순한 객체’에도 이런 메서드들을 사용할 수 있습니다.

let chineseDictionary = Object.create(null);
chineseDictionary.hello = "你好";
chineseDictionary.bye = "再见";

alert(Object.keys(chineseDictionary)); // hello,bye

요약

프로토타입을 설정하고 프로토타입에 접근하기 위한 모던한 방식은 다음과 같습니다.

사용자가 만든 키를 객체에 저장할 때, 내장 __proto__ getter/setter는 안전하지 않습니다. 사용자가 "__proto__"를 키로 입력할 수도 있기 때문에 에러가 생길 수 있습니다. 단순한 에러면 좋겠지만 보통 예측 불가능한 결과가 생깁니다.

그러므로 이럴 땐 Object.create(null)을 사용해 __proto__가 없는 '매우 단순한 객체’를 만들거나, 객체를 일관되게 사용하는 것이 좋습니다.

Object.create를 사용해 객체의 모든 설명자를 얕게 복사(shallow-copy)할 수 있습니다.

let clone = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));

__proto__[[Prototype]]의 getter/setter라는 점과 다른 메서드와 같이 Object.prototype에 정의되어 있다는 것도 확인했습니다.

Object.create(null)를 사용해 프로토타입이 없는 객체를 만들 수 있습니다. 이런 객체는 ‘순수히 사전으로’ 사용되고 "__proto__"를 키로 사용할 때도 문제가 없습니다.

살펴볼 만한 다른 메서드들은 다음과 같습니다.

객체의 프로퍼티를 반환하는 모든 메서드(Object.keys 등) 은 ‘객체가 소유한’ 프로퍼티만 반환합니다. 상속한 프로퍼티는 for..in를 사용해 얻을 수 있습니다.

과제

중요도: 5

key/value 쌍을 저장하기 위해 Object.create(null)로 생성된 dictionary 객체가 있습니다.

그 안에 쉼표로 구분된 키 목록을 반환하는 dictionary.toString()메서드를 추가하십시오. toString은 객체 위의 for..in에 나타나서는 안 됩니다.

작동 방식은 다음과 같습니다.

let dictionary = Object.create(null);

// dictionary.toString 메서드를 추가하는 코드

// 데이터를 추가합니다.
dictionary.apple = "Apple";
dictionary.__proto__ = "test"; // __proto__는 여기서 일반적인 프로퍼티 키입니다.

// 반복문에는 apple과 __proto__ 만 있습니다.
for(let key in dictionary) {
  alert(key); // "apple" 다음 "__proto__"입니다.
}

// toString이 동작하는 부분입니다.
alert(dictionary); // "apple,__proto__"

이 메서드는 Object.keys를 사용하여 열거 가능한 모든 키를 가져와서 목록으로 출력할 수 있습니다.

toString을 열거할 수 없도록 하기 위해 프로퍼티 설명자를 사용하여 toString을 정의하겠습니다. Object.create 문법을 사용하면 프로퍼티 설명자를 두 번째 인수로 사용하여 객체를 제공할 수 있습니다.

let dictionary = Object.create(null, {
  toString: { // toString 프로퍼티를 정의합니다.
    value() { // value는 함수입니다.
      return Object.keys(this).join();
    }
  }
});

dictionary.apple = "Apple";
dictionary.__proto__ = "test";

// apple과 __proto__는 반복문 안에 있습니다.
for(let key in dictionary) {
  alert(key); // "apple" 다음 "__proto__"가 있습니다.
}

// toString에 의해 쉼표로 구분된 프로퍼티 목록
alert(dictionary); // "apple,__proto__"

설명자를 사용하여 프로퍼티를 만들면 기본적으로 플래그가 false입니다. 따라서 위 코드에서 dictionary.toString은 열거할 수 없습니다.

다음 챕터 프로퍼티 플래그와 설명자를 참고하시기 바랍니다.

중요도: 5

새로운 rabbit 객체를 만들어 봅시다.

function Rabbit(name) {
  this.name = name;
}
Rabbit.prototype.sayHi = function() {
  alert(this.name);
};

let rabbit = new Rabbit("Rabbit");

아래와 같이 메서드를 호출하면 동일하게 동작할지 다르게 동작할지 예상해 보세요.

rabbit.sayHi();
Rabbit.prototype.sayHi();
Object.getPrototypeOf(rabbit).sayHi();
rabbit.__proto__.sayHi();

this는 실제 점 앞에 있는 객체를 나타내기 때문에, 첫 번째 호출에선 thisrabbit이고, 다른 호출에선 Rabbit.prototype입니다.

따라서 첫 번째 호출만이 Rabbit을 출력하고 다른 호출은 undefined를 출력합니다.

function Rabbit(name) {
  this.name = name;
}
Rabbit.prototype.sayHi = function() {
  alert( this.name );
}

let rabbit = new Rabbit("Rabbit");

rabbit.sayHi();                        // Rabbit
Rabbit.prototype.sayHi();              // undefined
Object.getPrototypeOf(rabbit).sayHi(); // undefined
rabbit.__proto__.sayHi();              // undefined
튜토리얼 지도

댓글

댓글을 달기 전에 마우스를 올렸을 때 나타나는 글을 먼저 읽어주세요.
  • 추가 코멘트, 질문 및 답변을 자유롭게 남겨주세요. 개선해야 할 것이 있다면 댓글 대신 이슈를 만들어주세요.
  • 잘 이해되지 않는 부분은 구체적으로 언급해주세요.
  • 댓글에 한 줄짜리 코드를 삽입하고 싶다면 <code> 태그를, 여러 줄로 구성된 코드를 삽입하고 싶다면 <pre> 태그를 이용하세요. 10줄 이상의 코드는 plnkr, JSBin, codepen 등의 샌드박스를 사용하세요.