개발을 하다보면, 기존 기능을 가져와 확장할 필요가 생깁니다.
자바스크립트의 고유 기능인 프로토타입 상속으로 이를 구현할 수 있습니다.
자바스크립트의 객체는 [[prototype]]
이라는 숨김 프로퍼티를 갖습니다.
이는 null이거나 객체만 허용하는데, 그 객체를 프로토타입이라 합니다.
객체에서 어떤 프로퍼티를 읽는데, 없다면 자동으로 프로토타입에서 찾습니다.
Element Node
가 Node
의 메서드를 사용할 수 있는 이유도 이와 같습니다.
[[prototype]]
은 숨김 프로퍼티 이지만, 여러 방법을 통해 설정할 수 있습니다.
__proto__
는 [[prototype]]
의 접근자 프로퍼티로 이 방법 중 하나입니다.
(다만, 다소 구식입니다. 표준에도 관련 내용이 명시되어 있습니다.)
let animal = {
eats : true
}
let rabbit = {
jumps : true,
ears : 2
}
rabbit.__proto__ = animal;
console.log(rabbit.eats); // true
rabbit
객체의 [[prototype]]
에서 animal
을 참조하기 때문에 eats
프로퍼티를 사용할 수 있습니다.
이러한 체인은 더 길어질 수 있습니다.
...-> rabbit -> mammal -> animal ->...
프로토타입의 프로퍼티가 enumerable:true
라면,
for...in
은 상속 받은 프로퍼티도 순회에 포함시킵니다.
obj.hasOwnProperty(key)
를 사용하면, obj의 직접 구현된 key만 true를 반환합니다.
이를 통해 구분할 수 있습니다.
for(let key in obj) {
let isOwn = obj.hasOwnProperty(key);
}
Object.keys(obj)
는 자신의 프로퍼티만 반환합니다.
Object.values
나 Object.entires
역시 상속 프로퍼티를 제외하고 동작합니다.
동일한 종류의 여러 객체를 생성할 때, 생성자 함수를 활용합니다.
생성자 함수와 new
연산자를 통해 객체를 생성할 수 있습니다.
생성하려는 객체가 상속을 받는다면,
모두 같은 객체를 상속받아야 할 것 입니다.
함수에 prototype
프로퍼티를 설정하면, 함수로 인해 생성되는 객체의 [[prototype]]
을 해당 객체로 설정합니다.
let animal = {
eats : true;
}
function Rabbit(name) {
this.name = name;
}
Rabbit.prototype = animal;
let rabbit = new Rabbit('bunny');
rabbit.eats // true
따로 할당하지 않아도 모든 함수는 prototype
프로퍼티를 갖습니다.
기본 값으로 constructor
프로퍼티 하나만 있는 객체를 가르킵니다.
또한, 이 constructor
는 함수 자기 자신을 가르킵니다.
function Rabbit(name) {
this.name = name;
}
// Rabbit.prototype = {controctor : Rabbit}
때문에, 생성된 객체를 통해서 새로운 객체를 만드는 것이 가능합니다.
let rabbit = new Rabbit('bunny');
let rabbit2 = new rabbit.contructor('tommy')
다만, 자바스크립트는 올바른 contructor
를 보장하지 않습니다
함수에 기본으로 해당 객체가 설정되지만, prototype
프로퍼티에 다른 값으로 덮어 씌우면,
contructor
가 없거나 변할 것입니다.
따라서, 이러한 상황을 방지하려면 prototype
프로퍼티 전체를 덮어 쓰기 보다
원하는 프로퍼티를 추가/제거
해야 합니다.
function Rabbit(){};
Rabbit.prototype.jumps = true;
// 기본 prototype의 contructor는 유지가 됨
우리는 객체를 출력하려 할 때, 문자로 형 변환 되는 과정을 알고 있습니다.
함수에 따라 hint
가 결정되고, 그에 따라 특정 메서드가 호출되어 반환합니다.
대부분은, toString()
메서드를 통해 "[object Object]"
라는 문자열로 변환됩니다.
let obj = {};
alert(obj); // "[object Object]"
그럼 여기서 toString()
은 어디 있을까요?
우리는 앞서, 객체의 찾을 수 없는 프로퍼티는 프로토타입에서 찾는다는 것을 배웠습니다.
let obj = {}
는 let obj = new Object()
의 축약입니다.
Object
는 내장 생성자 함수이며, 객체를 생성합니다.
이 생성자 함수의 prototype
프로퍼티가 toString()
을 비롯한 다양한 메서드가 구현된 객체를 참조합니다.
이로 인해, 이 생성자 함수로 생성된 객체가 프로토타입으로 Object.prototype
을 갖는 것입니다.
원시 값인 숫자, 문자, 불린은 객체가 아닙니다.
다만, 편의성을 위해 언어적으로 프로퍼티에 접근하는 것을 허용하였습니다.
원시 값의 프로퍼티의 접근하려 하면,
String
, Number
, Boolean
과 같은 내장 생성자가 래퍼 객체를 생성합니다.
래퍼(Wrapper) 객체는 메서드를 제공한 뒤 사라집니다.
이러한 래퍼 객체의 prototype
에 메서드들이 구현되어 있습니다.
문자열의 slice()
, split()
등이 이에 해당합니다.
마찬가지로 Array
, Date
, Fucntion
등 내장 객체들 역시 프로토타입에 메서드가 저장됩니다.
let arr = [1,2,3]
에서 join
등 메서드를 쓸 수 있는 이유가 이것입니다.
내장 객체들의 계층을 그리면 다음과 같습니다.
Object.prototype
의 위의 체인 [[prototype]]
은 없다는 걸 주의해야 합니다.
내장객체 프로토타입 역시 수정할 수 있습니다.
String.prototype.show() {
console.log(this);
}
이렇게 하면, 모든 문자열에서 해당 메서드를 사용할 수 있습니다.
하지만, 말 그대로 모든 문자열에서 사용할 수 있듯 프로토타입은 전역으로 영향을 미치기에 충돌 가능성이 커집니다.
네이티브 프로토타입을 수정하는 경우는 폴리필을 만들 때 뿐입니다.
__proto__
를 통해 프로토타입을 수정할 수 있지만, 호환성을 위해 남아있는 방식입니다.
대신, 메서드를 사용합니다.
Object.create(proto, [descriptors])
: [[prototype]]
이 proto
를 참조하는 객체를 만듦니다.Object.getPrototypeOf(obj)
: obj
의 [[prototype]]
을 반환합니다.Object.setPrototypeOf(obj, proto)
: 프로토타입을 설정합니다.create()
는 descriptors
도 인자로 받기 때문에, 프로토타입과 플래그까지 복사할 수 있습니다.
let clone = Object.creat(Object.getPrototypeOf(obj),Object.getOwnPropertyDescriptors(obj));
2015년에 getPrototypeOf
와 setPrototypeOf
가 표준에 추가되어, __proto__
가 대체 되었습니다.
그 이유는 무엇일까요?
__proto__
객체를 생성하면 Object.prototype
을 참조한 __proto__
접근자 프로퍼티가 생깁니다.
__proto__
에 접근하면, 대응되는 getter setter가 호출되어 [[prototype]]
을 읽거나 설정합니다.
때문에, __proto__
를 key로 사용할 수 없다는 결함을 가집니다.
객체나 null
외에는 무시되고, 객체를 넣으면 프로토타입이 바뀔 수 있습니다.
let plain = Object.creat(null);
이렇게 프로토타입이 없는 빈 객체를 만들면__proto__
는 getter와 setter를 상속받지 않습니다.
또한, __proto__
를 key로 사용할 수 있습니다.
이러한 객체는 아주 단순한 객체 혹은 순수 사전식 객체라고 부릅니다.
다만, 내장 메서드가 없다는 단점이 있습니다.
plain.toString = Object.prototype.toString;
내장 메서드는 이렇게 하면 가져올 수 있습니다.