본문 바로가기

모던 자바스크립트

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

프로토타입 메서드

◾️ __proto__ 대신 사용 할 수 있는 모던한 메서드

  1. Object.create(proto, [descriptors]) : [[Prototype]]이 proto를 참조하는 빈 객체를 만든다. 이 때, 프로퍼티 설명자를 추가로 넘길 수 있다. 
  2. Object.getPrototypeOf(obj) : obj의 [[Prototype]]을 반환한다. 
  3. Object.setPrototypeOf(obj, proto) : obj의 [[Prototype]]이 proto가 되도록 설정한다. 
let animal = {
  eats: true
};

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

console.log(rabbit.eats); // true
console.log(Object.getPrototypeOf(rabbit) === animal); // true

Object.setPrototypeOf(rabbit, {}); // rabbit의 prototype을 {}으로 바꾼다. 

◾️ Object.create에는 프로퍼티 설명자를 선택적으로 전달할 수 있다. 

let animal = {
  eats: true
};

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

console.log(rabbit.jumps); // true

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

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

◾️ Object.create를 호출하면 obj의 모든 프로퍼티를 포함한 완벽한 사본이 만들어진다. 사본엔 열거 가능한 프로퍼티와 불가능한 프로퍼티, 데이터 프로퍼티, getter, setter등 모든 프로퍼티가 복제된다. 

 

 

 

비하인드 스토리

◾️ Prototype을 다룰 수 있는 방법이 다양한 이유

  • 생성자 함수의 prototype 프로퍼티는 아주 오래전부터 그 기능을 수행하고 있었다. 
  • 그런데 2012년, 표준에 Object.create가 추가되었다. Object.create를 사용하면 주어진 프로토타입을 사용해 객체를 만들 수 있긴 하지만, 프로토타입을 얻거나 설정하는 것은 불가능했다. 그래서 브라우저는 비표준 접근자인 __proto__를 구현해 언제나 프로토타입을 얻거나 설정할 수 있도록 하였다. 
  • 이후 2015년에 Object.setPrototypeOf와 Object.getPrototypeOf가 표준에 추가되면서 __proto__와 동일한 기능을 수행할 수 있게 되었다. 그런데 이 시점엔 __proto__가 모든 곳에 구현되어 있어서 사실상 표준(de-facto standard)이 되어버렸다. 표준의 부록 B에 추가되기도 하였는데, 이 부록에 추가되면 브라우저가 아닌 환경에선 선택사항이란것을 의미한다. 

◾️ __proto__가 함수 getPrototypeOf/setPrototypeOf로 대체된 이유 

⚠️ 속도가 중요하다면 기존 객체의 [[Prototype]]을 변경하면 안된다. 

한다면 언제나 [[Prototype]]을 얻거나 설정할 수 있다. 하지만 대개는 객체를 생성할 때만 [[Prototype]]을 설정하고, 이후엔 수정하지 않는다. rabbit이 animal을 상속받도록 설정하고 난 이후엔 이를 변경하지 않는다. 

자바스크립트 엔진은 이런 시나리오를 토대로 최적화 되어 있다. Object.setPrototypeOf나 obj.__proto__=를 써서 프로토타입을 그때그때 바꾸는 연산은 객체 프로퍼티 접근 관련 최적화를 망치기 때문에 매우 느리다.

 

 

아주 단순한 객체

◾️ 객체는 키-값 쌍을 저장할 수 있는 연관 배열이다. 

◾️ 사용자가 직접 입력한 키를 가지고 객체를 만들다 보면 __proto__는 키로 사용할 수 없다는 결함이 발견된다.

let obj = {};

let key = prompt("입력하고자 하는 key는 무엇인가요?", "__proto__");
obj[key] = "...값...";

alert(obj[key]); [object Object]

◾️ 사용자가 프롬프트 창에 __proto__를 입력하면 값이 제대로 할당되지 않는다. 

◾️ __proto__ 는 항상 객체이거나 null이어야 한다. 문자열은 프로토타입이 될 수 없다. 

◾️ 키가 무엇이 되었든, 키-값 쌍을 저장하려고 하는데 키가 __proto__일 때 값이 제대로 저장되지 않는건 명백한 버그이다.

◾️ 할당 값이 객체일 때는 프로토타입이 바뀔 수 있다는 치명적인 버그가 발생할 수 있다. 프로토타입이 바뀌면 예상치 못한 일이 발생할 수 있기 때문이다. 

◾️ 서버 사이드에서 자바스크립트를 사용 중일 땐 이런 버그가 취약점이 되기도 한다. 

◾️ 객체 대신 맵을 사용하면 해결된다

◾️ 그런데 자바스크립트를 만든 사람들이 아주 오래전부터 이런 문제를 고려했기 때문에 객체를 써도 문제를 피할 수 있다.

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

◾️ 그렇기 때문에 obj.__proto__를 읽거나 쓸때는 이에 대응하는 getter, setter가 프로토타입에서 호출되고 [[Prototype]]을 가져오거나 설정한다. 

◾️ __proto__는 [[Prototype]]에 접근하기 위한 방법이지 [[Prototype]] 그 자체가 아니다. 

let obj = Object.create(null);

let key = prompt("입력하고자 하는 key는 무엇인가요?", "__proto__");
obj[key] = "...값...";

alert(obj[key]); // "...값..."

◾️ Object.create(null)을 사용해 [[Prototype]]이 null인 객체를 만든다. 

◾️ __proto__ getter와 setter를 상속받지 않는다. 

 

 

 

 

◾️ 이런 객체는 아주 단순한(very plain), 순수 사전식(pure dictionary)객체라고 부른다. 일반 객체 { ... }보다 훨씬 단순하기 때문이다. 

◾️ 아주 단순한 객체는 내장 메서드가 없기 때문에 toString같은 메서드를 사용할 수 없다. 

let obj = Object.create(null);

alert(obj); // Error: Cannot convert object to primitive value (toString이 없음)

◾️ 객체 관련 메서드 대부분은 Object.keys(obj) 같이 Object.something(...) 형태이다. 이 메서드들은 프로토타입에 있는게 아니기 때문에 아주 단순한 객체에도 사용할 수 있다. 

let koreanDictionary = Object.create(null);

koreanDictionary.hello = '안녕';
koreanDictionary.bye = '잘가';

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

 

 

📝 요약

◽️ 프로토타입에 직접 접근할 땐 다음과 같은 모던 메서드를 사용할 수 있다. 
    ◽️ Object.create(proto, [descriptors]) : [[Prototype]]이 proto인 객체를 만든다. 참조 값은 null일 수 있고 프로퍼티 설명자를 넘기는 것도 가능하다. 
    ◽️ Object.getPrototypeOf(obj) : obj의 [[Prototype]]을 반환한다. (__proto__ getter와 같다)
    ◽️ Object.setPrototypeOf(obj, proto) : obj의 [[Prototype]]을 proto로 설정한다. (__proto__ setter와 같다)

◽️ 사용자가 키를 직접 만들 수 있게 허용하면, 내장 __proto__ getter, setter는 안전하지 않다. 
    → key가 __proto__일 때 에러가 발생할 수 있다. 
◽️ 이를 방지하려면 Object.create(null)을 사용해 __proto__가 없는 아주 단순한 객체를 만들거나, 맵을 일관되게 사용하는 것이 좋다. 
◽️ Object.create를 사용하면 객체의 얕은 복사본(shallow-copy)을 만들 수 있다.

◽️ 더 살펴보면 좋은 메서드
     ◽️ Object.keys(obj) / Object.values(obj) / Object.entries(obj) : obj 내 열거 가능한 프로퍼티 키, 값, 키-값 쌍을 담은 배열을 반환한다.
     ◽️ Object.getOwnPropertySymbols(obj) : obj 내 심볼형 키를 담은 배열을 반환한다. 
     ◽️ Object.getOwnPropertyNames(obj) : obj 내 문자형 키를 담은 배열을 반환한다. 
     ◽️ Reflect.ownKeys(obj) : obj 내 키 전체를 담은 배열을 반환한다. 

     ◽️ obj.hasOwnProperty(key) : 상속받지 않고 obj 자체에 구현된 키 중 이름이 key인 것이 있으면 true를 반환
◽️ Object.keys를 비롯하여 객체의 프로퍼티를 반환하는 메서드들은 객체가 직접 소유한 프로퍼티만 반환합니다. 상속 프로퍼티는 for..in을 사용해 얻을 수 있다.

 

 

 


 

 

📌 [JAVASCRIPT_INFO 프로토타입 메서드와 __proto__가 없는 객체]

 

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

 

ko.javascript.info

 

 

 

 

 

 

'모던 자바스크립트' 카테고리의 다른 글

8.3 네이티브 프로토타입  (0) 2020.09.17
8.2 함수의 prototype 프로퍼티  (0) 2020.09.10
8.1 프로토타입 상속  (0) 2020.09.09
6.10 화살표 함수 다시 살펴보기  (0) 2020.09.02
6.10 함수 바인딩  (0) 2020.09.01