객체에는 숨겨진 속성인 [[Prototype]]이 존재한다. 이것은 null 또는 다른 객체(이것을 prototype이라고 부른다.)를 참조하고 있다.
closure가 어떤 변수를 찾아 outer lexical environment들로 탐색 해 나가는 것 처럼, 객체도 어떤 속성을 찾아 상위 prototype들을 탐색 해 나간다.
prototype을 수정하는 방법은 많이 있다. 그중에 하나는 아래와 같다.
const hero = {
strong: true
}
const superman = {
sex: male
}
superman.__proto__ = hero;
console.log(superman.strong) // true
예상 가능한 제한 사항이 있는데, 아래와 같다.
__proto__는[[Prototype]]이 아니다.[[Protorype]]의getter / setter이다.공식적으로는
Object.getPrototypeOf,Object.setPrototypeOf두 메소드를 사용하도록 권장하고 있다.
[[Prototype]]으로부터 상속 받은 속성들은 for ..in 루프에서 읽혀진다. 객체의 key 또는 value를 탐색하는 다른 메소드들은 보통 그렇지 않은데 말이다. 이것을 구분하는 메소드는 Object.hasOwnProperty(props) <Boolean>이다. 아래와 같이 사용할 수 있다.
let animal = {
eats: true
};
let rabbit = {
jumps: true,
__proto__: animal
};
for(let prop in rabbit) {
let isOwn = rabbit.hasOwnProperty(prop);
if (isOwn) {
console.log(`self-owned: ${prop}`); // self-owned: jumps
} else {
console.log(`Inherited: ${prop}`); // Inherited: eats
}
}
그런데 말이다, .hasOwnProperty()라는 메소드의 경우는 왜 for ..in 루프에서 신경도 안쓰는걸까? 예시에서 보이는 rabbit 객체가 가지고 있는 메소드도 아닌데 말이다.
그 비밀은 hasOwnProperty의 descriptor가 enumerable flag : false로 설정 되어있기 떄문이다. enumerable : true 가 for ..in 루프에 잡히는 조건이다.
만약 F.prototype이 객체라면, 해당 생성자 함수는 이것을 new operator로 만들어지는 객체들의 프로토타입으로 지정한다. 아래 예시를 보자.
let animal = {
eats: true
};
function Rabbit(name) {
this.name = name;
}
Rabbit.prototype = animal; // (1)
let rabbit = new Rabbit("White Rabbit"); // rabbit.__proto__ == animal
alert( rabbit.eats ); // true
// 옛날에는 이게 프로토타입을 지정하는 유일한 방법이어서, 오래된 스크립트에는 이런 방식이 쓰여있다.
그런데 위의 (1)에서처럼 생성자 함수의 prototype을 할당 해 주지 않아도 기본적으로 가지고 있기도 하다. (1)에서는 그것을 덮어쓰기 해 버린 것이다. 기본적인 prototype에도 유용한 속성이 들어있는데 constructor가 그것이다. new 연산자는 이 속성을 이용해서 객체를 생성 할 수 있다.
const rabbit2 = new rabbit.constructor("Red rabbit");
설령 생성자함수의 prototype이 null이 되어버린다 해도 함수의 이름으로 new 연산을 실행 할 수 있다. 그런데도 여전히 이 속성은 쓸모가 있는데, third party 라이브러리를 이용 하면서 어떤 객체가 무엇으로부터 만들어지는지 알아낼 때 등이다.
그래서 생성자 함수의 프로토타입은 덮어쓰기 보다는 추가하는 것이 좋다.
우리가 임의로 만드는 생성자 함수에도 default prototype이 있듯이, JS가 우리를 위해 기본적으로 세팅 해 두는 프로토타입들이 있다.
예를들어 리터럴 객체를 만든다고 해 보자. 이것을 브라우저 환경에서 alert()로 찍어보면 문자열로 형 변환이 되어 [object object]같은 모양새로 나온다. 이런 형 변환은 어디에 적혀있는 것인가? 바로 Native prototype에 정의되어 있다.
객체를 리터럴로 생성하는 것은 사실
Object 생성자 함수를new연산한 결과이다.const obj1 = {}; // (1) const obj2 = new Object(); // (2) // (1), (2)는 똑같은 실행이다
Object 생성자 함수가 가진 native 또는 default 프로토타입에 .toString 속성이 정의되어 있는 것이다. Array 나 Date 객체같은 놈들도 다 자기만의 native prototype을 가지고 있다.
모든 built-in prototype은 최상위 prototype으로
Object.prototype을 가지고 있다. 자바스크립트의 모든 것은 객체의 상속이라는 말은 이를 뜻한다.
String, Number and Boolean(원시타입 데이터)들은 객체가 아니다. 그러나 String.prototype, Number.prototype, Boolean.prototype으로부터 메소드를 얻는다. 전에도 공부했듯이, 얘들은 좀 특이하게도 일종의 decorator 또는 warpper 함수가 붙었다가 필요으면 떨어져나간다.(JS엔진이 최적화) 다만 null과 undefined는 예외다. 얘넨 아무것도 없다.
가능은 하다만, 특별한 경우가 아니면 건들지 않는게 좋다. 이유는 자명하니 생략하고... 특별한 경우는 예를들어 pollyfill같은 것이 있다.
.__proto__ 지금까지 예를 이놈으로 쓰긴 헀지만 이건 말하자면 deprecated 된 방식이다. 웹에서만 그나마 쓰는 놈이다.
앞으로는 아래의 방식으로 쓰면 좋다.
Object.getPrototypeOf(obj) // getter : 프로토타입을 읽어 온다.
Object.setPrototypeOf(obj, proto) // setter : 프로토타입을 덮어 쓴다.
(아래처럼)리터럴 객체를 정의 할 때 그나마 .__proto__를 쓰는데,
const obj = {
...,
__proto__ : ...
}
이마저도 아래의 방법이 표준이다.
Object.create(proto, [descriptors]) // proto를 프로토타입으로 가지는 빈 객체를
// 생성한다. optional하게 data properties
// 또는 accessor properties를 넣을 수 있다.
매우 순수한(?) 객체는 프로토타입이 없는 정말로 텅 빈 객체를 말한다. 바로 위 단원에서 알아본 객체 메소드를 써서 간단히 만들 수 있다.
const plainObj = Object.create(null);
이놈은 뭐 프로토타입을 비워버렸으니 내장 메소드도 없고 아무것도 없다. 문자열 형 변환 메소드를 출력해보면??
plainObj.toString // undefined
일반적인 경우, 어떤 객체에 key 값으로 __proto__를 주고 어떤 문자열을 할당(저장)하려 했다면 의도대로 작동하지 않았을 것이다. 왜냐면 __proto__는 객체 프로토타입의 getter 또는 setter이기 때문이다. very plain object라면 가능하다.