📢 기존에 있는 기능을 가져와 확장해야 하는 경우
사람에 관한 프로퍼티와 메서드를 가진 user라는 객체가 있는데, user와 상당히 유사하지만 약간의 차이가 있는 admin과 guest 객체를 만들어야 한다고 할 때, user의 메서드를 복사하거나 다시 구현하지 않고 프로토타입 상속(prototypal inheritance)을 이용하면 user에 약간의 기능을 얹어 admin과 guest객체를 만들 수 있다.
[[Prototype]]
◾️ 자바스크립트 객체는 명세서에서 명명한 [[Prototype]]이라는 숨김 프로퍼티를 가진다.
◾️ 숨김 프로퍼티 값은 null이거나 다른 객체에 대한 참조가 되는데, 다른 객체를 참조하는 경우 참조 대상을 프로토타입(prototype)이라고 한다.
◾️ 프로토타입 상속 : object에서 프로퍼티를 읽으려고 하는데 해당 프로퍼티가 없으면 자바스크립트는 자동으로 프로토타입에서 프로퍼티를 찾는다.
◾️ [[Prototye]] 프로퍼티는 내부 프로퍼티이면서 숨김 프로퍼티이지만 다양한 방법을 사용해 개발자가 값을 설정할 수 있다.
◾️ __proto__ 사용하여 값을 설정
let animal = {
eats: true
};
let rabbit = {
jumps: true
};
rabbit.__proto__ = animal;
ℹ️ __proto__는 [[Prototype]]용 getter, setter이다.
__proto__는 [[Prototype]]와 다르다. __proto__는 [[Prototype]]의 getter(획득자)이자 setter(설정자)이다.
하위 호환성 때문에 여전히 __proto__를 사용할 순 있지만 비교적 근래에 작성된 스크립트에선 __proto__대신 함수 Object.getPrototypeOf나 Object.setPrototypeOf을 써서 프로토타입을 획득(get)하거나 설정(set)한다.
__proto__는 브라우저 환경에서만 지원하도록 자바스크립트 명세서에서 규정하였는데, 실상은 서버 사이드를 포함한 모든 호스트 환경에서 __proto__를 지원한다.
◾️ 객체 rabbit에서 프로퍼티를 얻고싶은데 해당 프로퍼티가 없다면, 자동으로 animal이라는 객체에서 프로퍼티를 얻는다.
let animal = {
eats: true
};
let rabbit = {
jumps: true
};
rabbit.__proto__ = animal; // (*)
alert( rabbit.eats ); // true (**)
alert( rabbit.jumps ); // true
◾️ (*) : animal이 rabbit의 프로토타입이 되도록 설정
◾️ (**) : alert함수가 rabbit.eats 프로퍼티를 읽으려 했는데, rabbit엔 eats라는 프로퍼티가 없기 때문에 [[Prototype]]이 참조하고 있는 객체인 animal에서 eats를 얻어낸다.
➡️ rabbit의 프로토타입은 animal / rabbit은 animal을 상속받는다.
◾️ 프로토타입을 설정해주었기 때문에 rabbit에서도 animal에 구현된 프로퍼티와 메서드를 사용할 수 있다.
◾️ 상속 프로퍼티(inherited property) : 프로토타입에서 상속받은 프로퍼티
◾️ 프로토타입 animal에서 walk를 자동으로 상속 받았기 때문에 rabbit에서도 walk를 호출할 수 있게 된다.
let animal = {
eats: true,
walk() {
alert('동물이 걷는다');
}
};
let rabbit = {
jumps: true,
__proto__: animal
};
alert( rabbit.walk ); // 동물이 걷는다
◾️ 프로토타입 체인
let animal = {
eats: true,
walk() {
alert('동물이 걷는다');
}
};
let rabbit = {
jumps: true,
__proto__: animal
};
let longEar = {
earLength: 10,
__proto__: rabbit
};
longEar.walk(); // 동물이 걷는다 (animal -> rabbit에서 상속받음)
alert( longEar.jumps ); // true (rabbit에서 상속 받음)
◾️ 프로토타입 제약사항
- 순환 참조(circular reference)는 허용되지 않는다. __proto__를 이용해 닫힌 형태로 다른 객체를 참조하면 에러가 발생한다.
- __proto__의 값은 객체나 null만 가능하다. 다른 자료형은 무시된다.
쓸 때는 프로토타입을 사용하지 않는다.
◾️ 프로퍼타입은 프로퍼티를 읽을 때만 사용한다.
◾️ 프로퍼티를 추가, 수정거나 지우는 연산은 객체에 직접 해야 한다.
let animal = {
eats: true,
walk() {
/* rabbit은 이제 이 메서드를 사용하지 않는다. */
}
};
let rabbit = {
__proto__: animal
};
rabbit.walk = function () {
alert('토끼가 깡충깡충 뜁니다');
};
rabbit.walk(); // 토끼가 깡충깡충 뜁니다
◾️ rabbit.walk()를 호출하면 프로토타입에 있는 메서드가 실행되지 않고, 객체 rabbit에 추가한 메서드가 실행된다.
◾️ 접근자 프로퍼티(accessor property)는 setter함수를 통해서 프로퍼티에 값을 할당하므로 이 규칙이 적용되지 않는다. 접근자 프로퍼티에 값을 할당하는 것은 함수를 호출하는 것과 같기 때문이다.
let user = {
name: 'suzu',
surname: 'lee',
set fullName(value) {
[this.name, this.surname] = value.split(" ");
},
get fullName() {
return `${this.name} ${this.surname}`;
}
};
let admin = {
__proto__: user,
isAdmin: true
};
alert(admin.fullName); // suzu lee (*)
// setter 함수 실행
admin.fullName = 'matsumoto jun' // (**)
alert(admin.fullName); // matsumoto jun
◾️ (*) : admin.fullName은 프로토타입(user)에 있는 getter 함수(get fullName)를 호출하고
◾️ (**) : 프로토타입에 있는 setter 함수(set fullName)를 호출한다.
'this'가 나타내는 것
◾️ this는 프로토타입에 영향을 받지 않는다.
◾️ 메서드를 객체에서 호출했든 프로토타입에서 호출했든 상관없이 this는 언제나 .앞에 있는 객체가 된다.
◾️ admin.fullName = 으로 setter함수를 호출 할 때, this는 user가 아닌 admin이 된다.
◾️ 상속받은 메서드를 사용하더라도 객체는 프로토타입이 아닌 자신의 상태를 수정한다.
let animal = {
walk() {
if(!this.isSleeping) {
alert('동물이 걸어갑니다');
}
},
sleep() {
this.isSleeping = true;
}
};
let rabbit = {
name: '하얀 토끼',
__proto__: animal
};
// rabbit의 프로퍼티 isSleeping을 true로 변경한다.
rabbit.sleep();
alert( rabbit.isSleeping ); // true
alert( animal.isSleeping ); // undefined (프로토타입에는 isSleeping이라는 프로퍼티가 없다.)
◾️ rabbit 말고도 bird, snake 등이 animal을 상속받는다고 할 때, 이 객체들도 rabbit처럼 animal에 구현된 메서드를 사용할 수 있다. 이 때 상속받은 메서드의 this는 animal이 아닌 실제 메서드가 호출되는 시점의 점 앞에 있는 객체가 된다. 따라서 this에 데이터를 쓰면 animal이 아닌 해당 객체의 상태가 변화한다.
➡️ 메서드는 공유 되지만, 객체의 상태는 공유되지 않는다.
for...in 반복문
◾️ for...in은 상속 프로퍼티도 순회대상에 포함시킨다.
let animal = {
eats: true
};
let rabbit = {
jumps: true,
__proto__: animal
};
// Object.keys는 객체 자신의 키만 반환한다.
alert(Object.keys(rabbit)); // jumps
// for..in은 객체 자신의 키와 상속 프로퍼티의 키 모두를 순회한다.
for(let prop in rabbit) alert(prop); // jump, eats
◾️ obj.hasOwnProperty(key)를 이용하면 상속 프로퍼티를 순회 대상에서 제외할 수 있다. key에 대응하는 프로퍼티가 상속 프로퍼티가 아니고 obj에 직접 구현되어있는 프로퍼티일 때 true를 반환한다.
let animal = {
eats: true
};
let rabbit = {
jumps: true,
__proto__: animal
};
for(let prop in rabbit) {
let isOwn = rabbit.hasOwnProperty(prop);
if(isOwn) {
alert(`객체 자신의 프로퍼티: ${prop}`); // 객체 자신의 프로퍼티: jumps
} else {
alert(`상속 프로퍼티: ${prop}`); // 상속 프로퍼티: eats
}
}
◾️ rabbit은 animal을, animal은 Object.prototype을, Object.prototype은 null을 상속받고 있다. animal이 Object.prototype를 상속받는 이유는 객체 리터럴 방식으로 선언하였기 때문이다.
◾️ hasOwnProperty가 Object.prototype.hasOwnProperty에서 왔다.
◾️ hasOwnProperty는 상속 프로퍼티이지만 열거 가능한(enumerable) 프로퍼티가 아니기 때문에 출력이 되지 않는다. Object.prototype에 있는 모든 메서드의 enumerable 플래그는 false인데, for...in은 오직 열거 가능한 프로퍼티만 순회 대상에 포함하기 때문에 출력되지 않는다.
ℹ️ 키 - 값을 순회하는 메서드 대부분은 상속 프로퍼티를 제외하고 동작한다.
Object.keys, Object.values 같이 객체의 키-값을 대상으로 무언가를 하는 메서드 대부분은 상속 프로퍼티를 제외하고 동작한다. 프로토타입에서 상속받은 프로퍼티는 제외하고, 해당 객체에서 정의한 프로퍼티만 연산 대상에 포함한다.
📝 요약
◽️ 자바스크립트의 모든 객체엔 숨김 프로퍼티 [[Prototype]]이 있는데, 이 프로퍼티는 객체나 null을 가리킨다.
◽️ obj.__proto__를 사용하면 프로토타입에 접근할 수 있다. __proto__는 [[Prototype]]의 getter, setter로 쓰이는데, 요즘엔 잘 쓰지 않는다.
◽️ 프로토타입 : [[Prototype]]이 참조하는 객체
◽️ obj에서 프로퍼티를 읽거나 메서드를 호출하려는데 해당하는 프로퍼티나 메서드가 없으면 자바스크립트는 프로토타입에서 프로퍼티나 메서드를 찾는다.
◽️ 접근자 프로퍼티가 아닌 데이터 프로퍼티를 다루고 있다면, 쓰기나 지우기와 관련 연산은 프로토타입을 통하지 않고 객체에 직접 적용된다.
◽️ 프로토타입에서 상속받은 method라도 obj.method()를 호출하면 method안의 this는 호출 대상 객체인 obj를 가리킨다.
◽️ for..in 반복문은 객체 자체에서 정의한 프로퍼티 뿐만 아니라 상속 프로퍼티도 순회 대상에 포함된다. 반면, 키-값과 관련된 내장 메서드 대부분은 상속 프로퍼티는 제외하고 객체 자체 프로퍼티만을 대상으로 동작한다.
📝 오늘 새롭게 배운 내용 📝
✅ 프로토타입의 사전적 정의 : 원래의 형태 또는 전형적인 예, 기초 또는 표준
✅ 초기 모델을 정해놓고, 기본 특성은 그대로 이어 받으면서 조금씩 변형시켜서 완성해나갈 때 유용하다!
✅ 코드의 재사용성을 높일 수 있다.
✅ this는 프로토타입에 영향을 받지 않는다. 점 앞의 객체를 가리킨다.
✅ 참고하기 : [Javascript ] 프로토타입 이해하기
📌 [JAVASCRIPT_INFO 프로토타입 상속]
📌 [MDN Object.prototype]
'모던 자바스크립트' 카테고리의 다른 글
8.3 네이티브 프로토타입 (0) | 2020.09.17 |
---|---|
8.2 함수의 prototype 프로퍼티 (0) | 2020.09.10 |
6.10 화살표 함수 다시 살펴보기 (0) | 2020.09.02 |
6.10 함수 바인딩 (0) | 2020.09.01 |
6.9 call/apply와 데코레이터, 포워딩 (0) | 2020.08.31 |