어제 공부했던 __proto__
는 브라우저를 대상으로 개발하고 있다면 다소 구식이기 때문에 더는 사용하지 않는 것이 좋다고 한다. 표준에도 관련 내용이 명시되어있다.
대신 아래와 같은 최신 메소드들을 사용하는 것이 더 좋다.
[[Prototype]]
이 proto
를 참조하는 빈 객체를 만듭니다. 이때 프로퍼티 설명자를 추가로 넘길 수 있습니다.obj
의 [[Prototype]]
을 반환합니다.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의 프로토타입을 {}으로 바꾼다
Object.create
에는 프로퍼티 설명자를 선택적으로 전달할 수 있다. 설명자를 이용해 새 객체에 프로퍼티를 추가해보자!
let animal = {
eats: true
};
let rabbit = Object.create(animal, {
jumps: {
value: true
}
});
console.log(rabbit.jumps); // true
설명자는 프로퍼티 플래그와 설명자에서 배운 것과 같은 형태를 사용하면 된다. https://velog.io/@toffg6450/Study-JavaScript-0624-%EA%B0%9D%EC%B2%B4-%ED%94%84%EB%A1%9C%ED%8D%BC%ED%8B%B0-%EC%84%A4%EC%A0%95
Object.create
를 사용하면 for..in
을 사용해 프로퍼티를 복사하는 것보다 더 효과적으로 객체를 복제할 수 있다.
let clone = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));
[[Prototype]]
을 다루는 방법은 다양하다. 이유가 무엇일까?
이는 역사적인 이유가 있다고 한다.
"prototype"
프로퍼티는 아주 오래전부터 사용되고 있었다.Object.create
가 추가되었다. Object.create
를 사용하면 주어진 프로토타입을 사용해 객체를 만들 수 있긴 하지만, 프로토타입을 얻거나 설정하는것은 불가능했다. 그래서 브라우저는 비표준 접근자인 __proto__
를 구현해 언제나 프로토타입을 얻거나 설정할 수 있도록 했다.Object.setPrototypeOf
와 Object.getPrototypeOf
가 표준에 추가되면서 __proto__
와 동일한 기능을 수행할 수 있게 되었다. 그런데 이 시점엔 __proto__
를 사용하는 곳이 너무 많아서 __proto__
는 사실상 표준(de-facto standard)이 되버렸다. 이 내용은 명세서의 부록 B(Annex B)에 추가되어 있다. 부록 B의 내용은 브라우저 이외의 호스트 환경에선 선택사항이라는것을 의미힌다.이런 역사적인 이유 때문에 지금은 여러 방식을 원하는 대로 쓸 수 있게되었다고 한다.
※ 속도가 중요하다면 기존 객체의 [[Prototype]]을 변경하지 말자.
원한다면 언제나 [[Prototype]]을 얻거나 설정할 수 있다. 기술적 제약이 있는 건 아니지만 대개는 객체를 생성할 때만 [[Prototype]]을 설정하고 이후엔 수정하지 않는다.
javascript 엔진은 이런 시나리오를 토대로 최적화되어 있다.
Object.setPrototypeOf
나obj.__proto__=
를 써서 프로토타입을 그때그때 바꾸는 연산은 객체 프로퍼티 접근 관련 최적화를 망치기 때문에 성능에 나쁜 영향을 미친다.
알다시피 객체는 key-value 쌍이 있는 연관 배열로도 사용할 수 있다.
그런데 커스텀 사전을 만드는 것과 같이 사용자가 직접 입력한 key를 가지고 객체를 만들다 보면 사소한 결함이 발견되는데, 다른 문자열은 괜찮지만 "__proto__"
라는 문자열은 키로 사용할 수 없다는 결함이다.
let obj = {};
let key = prompt("입력하고자 하는 key는 무엇인가요?", "__proto__");
obj[key] = "...값...";
console.log(obj[key]); // "...값..."이 아닌 [object Object]가 출력
프롬프트 창에 __proto__
를 입력하면 값이 제대로 할당되지 않는것을 확인할 수 있다. __proto__
프로퍼티는 특별한 프로퍼티이기에 놀랄만한 일은 아닐 수 있다.
__proto__
는 항상 객체이거나 null
이어야 합니다. 문자열은 프로토타입이 될 수 없다.
다시 돌아와서 key가 무엇이 되었든, key-value 쌍을 저장하려고 하는데 key가 __proto__
일 때 값이 제대로 저장되지 않는 건 명백한 버그다.
예시에선 이 버그가 그리 치명적이진 않지만 할당 값이 객체일 때는 프로토타입이 바뀔 수 있다는 치명적인 버그가 발생할 수 있다. 프로토타입이 바뀌면 예상치 못한 일이 발생할 수 있기 때문이다.
개발자들은 대개 프로토타입이 중간에 바뀌는 시나리오는 배제한 채 개발을 진행하며, 이런 고정관념 때문에 프로토타입이 중간에 바뀌면서 발생한 버그는 그 원인을 쉽게 찾지 못한다. 서버 사이드에서 javascript를 사용 할 땐 이런 버그가 취약점이 되기도 한다.
toString
을 비롯한 내장 메서드에 할당을 할 때도 같은 이유 때문에 예상치 못한 일이 일어날 수 있다.
우리는 이 문제가 일어나지 않게 예방하기 위해 객체 대신 맵을 사용하자
그런데 javascript를 만든 사람들이 아주 오래전부터 이런 문제를 고려했기 때문에 객체를 써도 문제를 예방할 수 있다.
그렇다면 객체를 써서 문제를 예방하는 방법을 알아보자!
아시다시피__proto__
는 객체의 프로퍼티가 아니라 Object.prototype
의 접근자 프로퍼티이다.
그렇기 때문에 obj.__proto__
를 읽거나 쓸때는 이에 대응하는 getter, setter가 프로토타입에서 호출되고 obj
는 [[Prototype]]
을 통해 getter와 setter에 접근한다.
이 절을 시작할 때 언급한 것처럼 __proto__
는 [[Prototype]]
에 접근하기 위한 수단이지 [[Prototype]]
그 자체가 아닌 것이다!
이제 간단한 트릭을 써 객체가 연관 배열의 역할을 다 할 수 있도록 해보자!
let obj = Object.create(null);
let key = prompt("입력하고자 하는 key는 무엇인가요?", "__proto__");
obj[key] = "...값...";
console.log(obj[key]); // "...값..."이 제대로 출력
Object.create(null)
을 사용해 프로토타입이 없는 빈 객체를 만들어 보았다. [[Prototype]]
이 null
인 객체를 만든 것이다.
Object.create(null)
로 객체를 만들면 __proto__
getter와 setter를 상속받지 않습니다. 이제 __proto__
는 평범한 데이터 프로퍼티처럼 처리되므로 버그 없이 예시가 잘 동작하게 된다.
이렇게 프로토타입이 없는 빈 객체는 ‘아주 단순한(very plain)’ 혹은 ‘순수 사전식(pure dictionary)’ 객체라고 부른다. 일반 객체 {...}
보다 훨씬 단순하다.
참고로 아주 단순한 객체는 내장 메서드가 없다는 단점이 있다. toString
같은 메서드를 사용할 수 없다.
let obj = Object.create(null);
console.log(obj); // Error: Cannot convert object to primitive value (toString이 없음)
객체를 연관 배열로 쓸 때는 이런 단점이 문제가 되진 않는다.
객체 관련 메서드 대부분은 Object.keys(obj)
같이 Object.something(...)
형태를 띈다. 이 메서드들은 프로토타입에 있는 게 아니기 때문에 '아주 단순한 객체’에도 사용할 수 있다.
let chineseDictionary = Object.create(null);
chineseDictionary.hello = "你好";
chineseDictionary.bye = "再见";
console.log(Object.keys(chineseDictionary)); // hello,bye
우리는 리터럴 뿐만 아니라 new F()
와 같은 생성자 함수로도 새로운 객체를 만들 수 있다는 걸 배운 바 있다.
이번에는 생성자 '함수’를 사용해 객체를 만든 경우에 프로토타입이 어떻게 동작하는지에 대해 알아보자!
생성자 함수로 객체를 만들었을 때 리터럴 방식과 다른점은 생성자 함수의 프로토타입이 객체인 경우에 new
연산자를 사용해 만든 객체는 생성자 함수의 프로토타입 정보를 사용해 [[Prototype]]
을 설정한다는 것이다.
생성자 함수(F
)의 프로토타입을 의미하는 F.prototype
에서 "prototype"
은 F
에 정의된 일반 프로퍼티라는 점에 주의하자.
F.prototype
에서 "prototype"
은 바로 앞에서 배운 '프로토타입’과 비슷하게 들리겠지만 이름만 같을 뿐 실제론 다른 우리가 익히 알고있는 일반적인 프로퍼티이다.
예시:
let animal = {
eats: true
};
function Rabbit(name) {
this.name = name;
}
Rabbit.prototype = animal;
let rabbit = new Rabbit("흰 토끼"); // rabbit.__proto__ == animal
console.log( rabbit.eats ); // true
Rabbit.prototype = animal
은 "new Rabbit
을 호출해 만든 새로운 객체의 [[Prototype]]
을 animal
로 설정하라."는 것을 의미한다.
그림으로 나타내면 다음과 같다.
여기서 가로 화살표는 일반 프로퍼티인 "prototype"
을, 세로 화살표는 [[Prototype]]
을 나타냅니다. 세로 화살표는 rabbit
이 animal
을 상속받았다는 것을 의미한다.
※
F.prototype
은new F
를 호출할 때만 사용된다.
new F
를 호출할 때 만들어지는 새로운 객체의[[Prototype]]
을 할당해준다.
새로운 객체가 만들어진 후에F.prototype
프로퍼티가 바뀌면(F.prototype = <another object>
)new F
를 호출해 만드는 또 다른 새로운 객체는 another object를[[Prototype]]
으로 갖게 된다. 다만, 기존에 있던 객체의[[Prototype]]
은 그대로 유지된다.
모든 함수는 기본적으로 1"prototype"1 프로퍼티를 갖는다.
디폴트(지정하지 않아도 자동으로 선택되는 무언가) 프로퍼티 "prototype"
은 constructor
프로퍼티 하나만 있는 객체를 가리키는데, 여기서 constructor
프로퍼티는 함수 자신을 가리킨다.
이 관계를 코드와 그림으로 나타내면 다음과 같습니다.
function Rabbit() {}
/* 디폴트 prototype
Rabbit.prototype = { constructor: Rabbit };
*/
예시를 실행해 직접 확인해보자!
function Rabbit() {}
// 함수를 만들기만 해도 디폴트 프로퍼티인 prototype이 설정된다.
// Rabbit.prototype = { constructor: Rabbit }
console.log( Rabbit.prototype.constructor == Rabbit ); // true
특별한 조작을 가하지 않았다면 new Rabbit
을 실행해 만든 토끼 객체 모두에서 constructor
프로퍼티를 사용할 수 있는데, 이때 [[Prototype]]
을 거친다.
function Rabbit() {}
// 디폴트 prototype:
// Rabbit.prototype = { constructor: Rabbit }
let rabbit = new Rabbit(); // {constructor: Rabbit}을 상속받음
console.log(rabbit.constructor == Rabbit); // true ([[Prototype]]을 거쳐 접근함)
constructor
프로퍼티는 기존에 있던 객체의 constructor
를 사용해 새로운 객체를 만들때 사용할 수 있다.
function Rabbit(name) {
this.name = name;
console.log(name);
}
let rabbit = new Rabbit("흰 토끼");
let rabbit2 = new rabbit.constructor("검정 토끼");
이 방법은 객체가 있는데 이 객체를 만들 때 어떤 생성자가 사용되었는지 알 수 없는 경우(객체가 서드 파티 라이브러리에서 온 경우 등) 유용하게 쓸 수 있다.
"constructor"
를 이야기 할 때 가장 중요한 점은
javascript는 알맞은 "constructor"
값을 보장하지 않는다는 점이다.
함수엔 기본으로 "prototype"
이 설정된다는게 전부다. "constructor"
와 관련해서 벌어지는 모든 일은 전적으로 개발자에게 달려있다.
함수에 기본으로 설정되는 "prototype"
프로퍼티 값을 다른 객체로 바꿔 무슨일이 일어나는지 살펴보자. new
를 사용해 객체를 만들었지만 이 객체에 "constructor"
가 없는 것을 확인할 수 있다.
function Rabbit() {}
Rabbit.prototype = {
jumps: true
};
let rabbit = new Rabbit();
console.log(rabbit.constructor === Rabbit); // false
이런 상황을 방지하고 constructor
의 기본 성질을 제대로 활용하려면 "prototype"
에 뭔가를 하고 싶을 때 "prototype"
전체를 덮어쓰지 말고 디폴트 "prototype"
에 원하는 프로퍼티를 추가, 제거해야 한다.
function Rabbit() {}
// Rabbit.prototype 전체를 덮어쓰지 말고
// 원하는 프로퍼티가 있으면 그냥 추가한다.
Rabbit.prototype.jumps = true
// 이렇게 하면 디폴트 프로퍼티 Rabbit.prototype.constructor가 유지된다.
실수로 "prototype"
을 덮어썼다 하더라도 constructor
프로퍼티를 수동으로 다시 만들어주면 constructor
를 다시 사용할 수 있다.
Rabbit.prototype = {
jumps: true,
constructor: Rabbit
};
// 수동으로 constructor를 추가해 주었기 때문에 우리가 알고 있던
// constructor의 특징을 그대로 사용할 수 있다.