요약
- 모든 내장 객체는 같은 패턴을 따릅니다.
- 메서드는 프로토타입에 저장됩니다(Array.prototype
,Object.prototype
,Date.prototype
등).
- 객체 자체엔 데이터만 저장합니다(배열의 요소, 객체의 프로퍼티, 날짜 등).- 원시값 또한 래퍼 객체의 프로토타입에
Number.prototype
,String.prototype
,Boolean.prototype
같은 메서드를 저장합니다.
undefined
와null
값은 래퍼 객체를 가지지 않습니다.- 내장 프로토타입을 수정할 수 있습니다. 내장 프로토타입의 메서드를 빌려와 새로운 메서드를 만드는 것 역시 가능합니다. 그러나 내장 프로토타입 변경은 되도록 하지 않아야 합니다. 내장 프로토타입은 새로 명세서에 등록된 기능을 사용하고 싶은데 자바스크립트 엔진엔 이 기능이 구현되어있지 않을 때만 변경하는 게 좋습니다.(폴리필 적용 시에만)
obj = new Object()
를 줄이면 obj = {}
가 됩니다. 여기서 Object
는 내장 객체 생성자 함수인데, 이 생성자 함수의 prototype
은 toString
을 비롯한 다양한 메서드가 구현되어있는 거대한 객체를 참조합니다.
new Object()
를 호출하거나 리터럴 문법 {...}
을 사용해 객체를 만들 때, 새롭게 생성된 객체의 [[Prototype]]
은 이전 챕터에서 언급한 규칙에 따라 Object.prototype
을 참조합니다.
따라서 obj.toString()
을 호출하면 Object.prototype
에서 해당 메서드를 가져오게 되죠.
let obj = {};
alert(obj.__proto__ === Object.prototype); // true
alert(obj.toString === obj.__proto__.toString); //true
alert(obj.toString === Object.prototype.toString); //true
Object.prototype
위의 체인엔 [[Prototype]]
이 없다는 점을 주의하셔야 합니다.Array
, Date
, Function
을 비롯한 내장 객체들 역시 프로토타입에 메서드를 저장해 놓습니다.[1, 2, 3]
을 만들면 기본 new Array()
생성자가 내부에서 사용되기 때문에 Array.prototype
이 배열 [1, 2, 3]
의 프로토타입이 되죠. Array.prototype
은 배열 메서드도 제공합니다. 이런 내부 동작은 메모리 효율을 높여주는 장점을 가져다줍니다.Object.prototype
이 있어야 한다고 규정합니다. 이런 규정 때문에 몇몇 사람들은 "모든 것은 객체를 상속받는다."라는 말을 하죠.let arr = [1, 2, 3];
// arr은 Array.prototype을 상속받았나요?
alert( arr.__proto__ === Array.prototype ); // true
// arr은 Object.prototype을 상속받았나요?
alert( arr.__proto__.__proto__ === Object.prototype ); // true
// 체인 맨 위엔 null이 있습니다.
alert( arr.__proto__.__proto__.__proto__ ); // null
Array.prototype
엔 요소 사이에 쉼표를 넣어 요소 전체를 합친 문자열을 반환하는 자체 메서드 toString
가 있습니다.let arr = [1, 2, 3]
alert(arr); // 1,2,3 <--Array.prototype.toString 의 결과
Object.prototype
에도 메서드 toString
이 있습니다. 이렇게 중복 메서드가 있을 때는 체인 상에서 가까운 곳에 있는 메서드가 사용됩니다. Array.prototype
이 체인 상에서 더 가깝기 때문에 Array.prototype
의 toString
이 사용되죠.String
, Number
, Boolean
을 사용하는 임시 래퍼(wrapper) 객체가 생성됩니다. 임시 래퍼 객체는 이런 메서드를 제공하고 난 후에 사라집니다.String.prototype
, Number.prototype
, Boolean.prototype
을 사용해 쓸 수 있도록 규정합니다.
null
과undefined
에 대응하는 래퍼 객체는 없습니다.
String.prototype
에 메서드를 하나 추가하면 모든 문자열에서 해당 메서드를 사용할 수 있죠.String.prototype.show = function() {
alert(this);
};
"BOOM!".show(); // BOOM!
🚨 프로토타입은 전역으로 영향을 미치기 때문에 프로토타입을 조작하면 충돌이 날 가능성이 높습니다. 두 라이브러리에서 동시에
String.prototype.show
메서드를 추가하면 한 라이브러리의 메서드가 다른 라이브러리의 메서드를 덮어쓰죠.
이런 이유로 네이티브 프로토타입을 수정하는 것을 추천하지 않습니다.
let obj = {
0: "Hello",
1: "world!",
length: 2,
};
obj.join = Array.prototype.join;
alert( obj.join(',') ); // Hello,world!
join
의 내부 알고리즘은 제대로 된 인덱스가 있는지와 length
프로퍼티가 있는지만 확인하기 때문입니다. 호출 대상이 진짜 배열인지는 확인하지 않죠. 다수의 내장 메서드가 이런 식으로 동작합니다.obj.__proto__
를 Array.prototype
로 설정해 배열 메서드를 상속받는 방법이 있습니다. 이렇게 하면 obj
에서 모든 Array
메서드를 사용할 수 있습니다.obj
가 다른 객체를 상속받고 있을 때는 사용할 수 없습니다. 자바스크립트는 단일 상속만을 허용한다는 점을 기억하시기 바랍니다.프로토타입에 직접 접근할 땐 다음과 같은 모던 메서드를 사용할 수 있습니다.
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
는 안전하지 않습니다. 키가 "__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(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
을 사용해 얻을 수 있습니다.
__proto__
는 브라우저를 대상으로 개발하고 있다면 다소 구식이기 때문에 더는 사용하지 않는 것이 좋습니다. 표준에도 관련 내용이 명시되어있습니다.Object.create(proto, [descriptors])
[[Prototype]]
이 proto
를 참조하는 빈 객체를 만듭니다. 이때 프로퍼티 설명자를 추가로 넘길 수 있습니다.Object.getPrototypeOf(obj)
obj
의 [[Prototype]]
을 반환합니다.Object.setPrototypeOf(obj, proto)
obj
의 [[Prototype]]
이 proto
가 되도록 설정합니다.let animal = {
eats: true
};
// 프로토타입이 animal인 새로운 객체를 생성합니다.
// Rabbit.prototype = animal;
// let rabbit = new Rabbit("White Rabbit");
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
을 사용해 프로퍼티를 복사하는 것보다 더 효과적으로 객체를 복제할 수 있습니다.let clone = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));
Object.create
를 호출하면 obj
의 모든 프로퍼티를 포함한 완벽한 사본이 만들어집니다, 사본엔 열거 가능한 프로퍼티와 불가능한 프로퍼티, 데이터 프로퍼티, getter
, setter
등 모든 프로퍼티가 복제됩니다. [[Prototype]]
도 복제되죠.
[[Prototype]]을 다룰 수 있는 방법은 다양한 이유는 역사적으로 __proto__
가 Legacy
가 된 케이스이기 때문.
속도가 중요하다면 기존 객체의 [[Prototype]]을 변경하지 마세요. 객체 프로퍼티 접근 관련 최적화를 망칠 수 있기 때문
__proto__
는 키로 사용할 수 없다는 결함이죠.let obj = Object.create(null);
let key = prompt("입력하고자 하는 key는 무엇인가요?", "__proto__");
obj[key] = "...값...";
alert(obj[key]); // "...값..."이 제대로 출력됩니다.
__proto__
getter
와 setter
를 상속받지 않습니다. 이제 __proto__
는 평범한 데이터 프로퍼티처럼 처리되므로 버그 없이 예시가 잘 동작하게 됩니다.{...}
보다 훨씬 단순하기 때문이죠.