프로토타입과 프로토타입 상속
1. 프로토타입 상속
개발을 하다 보면 기존에 있는 기능을 가져와 확장해야 하는 경우가 생긴다
예를 들어 user
라는 객체가 있는데 이 user
와 굉장히 유사하지만 약간의 차이가 있는 객체를 만들어야 된다고 생각해보면 어떤 방식이 가장 효율적일까?
그 방식 중 하나가 바로 프로토타입 상속(prototypal inheritance)이다
[[prototype]]
자바스크립트의 객체는 [[prototype]]
이라는 숨김 프로퍼티를 갖는다
이 값은 null
이거나 다른 객체에 대한 참조가 되는데 다른 객체를 잠초하는 경우 참조대상을 '프로토타입(prototype)'이라 부른다
프로토타입에 재밌는 점은 위의 그림에서 object
에서 프로퍼티를 읽으려고 하는데 해당 프로퍼티가 없으면 자동으로 프로토타입에서 프로퍼티를 찾는다는 점이다
프로그래밍에서 이런 동작 방식을 prototypal inheritance
라고 한다
[[prototype]]
은 숨겨져 있는 프로퍼티이지만 다양한 방법으로 개발자가 값을 설정할 수 있는데 이를 위해 아래와 같이 __proto__
를 사용한다
let animal = {
eats: true
};
let rabbit = {
jumps: true
};
rabbit.__proto__ = animal;
__proto__
는[[prototype]]
용 getter-setter다
즉, 값을 얻어오거나 변경할 수 있을 뿐__proto__ === [[prototype]]
이라는 말이 아니다
최근에는__proto__
대신Object.getPrototypeOf
로 값을 얻어오고
Object.setPrototypeOf
을 사용하여 값을 설정한다
⚠ (단, 호환성 문제로 여전히__proto__
를 사용하는 경우도 많다)
위의 예시에서 rabbit
에는 eats: true
가 없지만 프로토타입으로 animal
로 설정했기 때문에 해당 객체에서 프로퍼티를 가져올 수 있다
let animal = {
eats: true
};
let rabbit = {
jumps: true
};
rabbit.__proto__ = animal; // (*)
// 프로퍼티 eats과 jumps를 rabbit에서도 사용할 수 있게 되었습니다.
alert( rabbit.eats ); // true (**)
alert( rabbit.jumps ); // true
이러한 방식은 메서드에도 적용이 가능하다
let animal = {
eats: true,
walk() {
alert("동물이 걷습니다.");
}
};
let rabbit = {
jumps: true,
__proto__: animal
};
// 메서드 walk는 rabbit의 프로토타입인 animal에서 상속받았습니다.
rabbit.walk(); // 동물이 걷습니다.
프로토타입의 깊이(depth)는 무한히 깊어질 수 있지만 이를 권장하지는 않는다
let animal = {
eats: true,
walk() {
alert("동물이 걷습니다.");
}
};
let rabbit = {
jumps: true,
__proto__: animal
};
let longEar = {
earLength: 10,
__proto__: rabbit
};
// 메서드 walk는 프로토타입 체인을 통해 상속받았습니다.
longEar.walk(); // 동물이 걷습니다.
alert(longEar.jumps); // true (rabbit에서 상속받음)
프로토타입 체이닝에는 제약사항이 있는데
- 순환 참조(circular reference)를 허용하지 않는다
즉, 닫힌 형태로 다른 객체를 참조하면 에러가 발생한다 __proto__
의 값은 객체나null
만 가능하며 다른 자료형은 무시된다- 객체엔 오직 하나의
[[prototype]]
만 있을 수 있다
즉, 객체는 두 개의 객체를 동시에 상속받지 못한다
const animal = {
walk: true
}
const human = {
talk: true
}
const ayaan = {
eat: true
}
ayaan.__proto__ = animal;
console.log(ayaan.walk); // true
// 하나의 객체만 상속받을 수 있음
ayaan.__proto__ = human;
console.log(ayaan.walk); // undefined
console.log(ayaan.talk); // true
// 순환 참조
human.__proto__ = animal;
animal.__proto__ = ayaan;
// VM570:1 Uncaught TypeError: Cyclic __proto__ value
"프로토타입은 읽기 전용이다" 이 말을 꼭 명심하도록 하자
프로토타입은 프로퍼티를 읽을 때만 사용한다
즉, 프로퍼티를 추가, 수정하거나 지우는 연산은 객체에 직접 해야한다
그리고 프로토타입은 상속받는 객체에 해당 프로퍼티가 없을 때에만 상속을 해준다
let animal = {
eats: true,
walk() {
/* rabbit은 이제 이 메서드를 사용하지 않습니다. */
}
};
let rabbit = {
__proto__: animal
};
rabbit.walk = function() {
alert("토끼가 깡충깡충 뜁니다.");
};
rabbit.walk(); // 토끼가 깡충깡충 뜁니다.
하지만 접근자 프로퍼티(accessor property)의 경우 조금 다르게 작동한다
let user = {
name: "John",
surname: "Smith",
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); // John Smith (*)
// setter 함수가 실행됩니다!
admin.fullName = "Alice Cooper"; // (**)
alert(admin.fullName); // Alice Cooper, setter에 의해 추가된 admin의 프로퍼티(name, surname)에서 값을 가져옴
alert(user.fullName); // John Smith, 본래 user에 있었던 프로퍼티 값
위 코드를 이해하기 위해서는 아무래도 this
에 대한 이해가 더 필요할 것 같다
2. this
위 예시에서 this
에는 어떤 값이 들어갈까?
결론부터 말하면 this
는 프로토타입이 영향을 받지 않는다
메서드를 객체에서 호출했든 프로토타입에서 호출했든 상관없이 this
는 언제나 .
앞에 있는 객체를 의미한다
예를 들어 위에서 admin.fullName = ...
으로 setter함수를 호출할 때 this
는 admin
이다
이러한 특성은 거대한 객체를 하나 만들고 여기서 많은 메서드들을 상속받게 하는 경우 용이하게 사용된다
예를 들어 메서드 저장소 역할을 하는 객체 animal
을 rabbit
이 상속받는다고 한다면
// animal엔 다양한 메서드가 있습니다.
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이라는 프로퍼티가 없습니다.)
'메서드는 공유되지만 객체의 상태는 공유되지 않게 하려면 this를 사용하자'
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); // jumps, eats
obj.hasOwnProperty(key)
를 사용해 boolean value로 상속받은 프로퍼티인지 자신의 프로퍼티인지를 확인할 수 있다
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
}
}
여기서 재밌는 점은 animal
은 객체 리터럴 방식으로 선언되었기 때문에 Object.prototype
을 상속받는다
그렇기 때문에 rabbit
이 rabbit.hasOwnProperty(key)
를 갖고 있을 수 있는 것이다
MDN documentation을 보면
Object.prototype.someMethod()
와 같은 형식으로 나열되어 있는데 그러한 이유가Object.prototype
을 상속받기 때문이다
그런데 이상하지 않는가? hasOwnProperty
메서드는 for...in
으로 열거할 수 없었는데 그 이유가 무엇일까?
hasOwnProperty)
같은 메서드는 Property flags and descriptors에서 알아봤던 enumerable
이 false
이기 때문이다
3. 생성자 함수에서의 prototype
위에서 리터럴로 객체를 생성하게 되면 Object.prototype
을 상속받는다고 했는데 그렇다면 new F()
와 같이 생성자 함수를 통해 생성된 객체의 프로토타입은 어떨까?
생성자 함수(F
)의 프로토타입을 의미하는 F.prototype
에서 prototype
은 F
에 정의된 일반 프로퍼티라는 점이 중요하다
F.prototype
에서 prototype
은 바로 앞에서 배운 [[Prototype]]
과 비슷하게 들리겠지만 이름만 같을 뿐 실제론 다르다
let animal = {
eats: true
};
function Rabbit(name) {
this.name = name;
}
Rabbit.prototype = animal;
let rabbit = new Rabbit("흰 토끼"); // rabbit.__proto__ == animal
alert(rabbit.eats); // true
console.dir(rabbit);
console.dir(Rabbit);
생성자 함수와 생성자 함수를 통해 생성된 인스턴스의 구조적 차이를 console.dir()
를 통해 알 수 있는데 이 차이를 그림으로 보면 다음과 같다
함수의 prototype
은 사실 특별히 할당하지 않더라도 모든 함수가 가지고 있는 프로퍼티다
default는 constructor
이며, constructor
는 함수 자신을 가리킵니다
특별한 조작을 가하지 않았다면 new Rabbit
을 실행해 만든 토끼 객체 모두에서 constructor
프로퍼티를 사용할 수 있는데 이때 [[Prototype]]
을 거친다
function Rabbit() {}
// 디폴트 prototype:
// Rabbit.prototype = { constructor: Rabbit }
let rabbit = new Rabbit(); // {constructor: Rabbit}을 상속받음
alert(rabbit.constructor == Rabbit); // true ([[Prototype]]을 거쳐 접근함)
constructor
를 사용하기 위해서 처음과 같이 완전히 새로운 객체를 할당하기 보다는 prototype
에 원하는 프로퍼티를 추가, 제거하는 것이 좋다
function Rabbit() {}
// Rabbit.prototype 전체를 덮어쓰지 말고
// 원하는 프로퍼티가 있으면 그냥 추가합니다.
Rabbit.prototype.jumps = true
// 이렇게 하면 디폴트 프로퍼티 Rabbit.prototype.constructor가 유지됩니다.
만약 실수로 constructor
를 제거하였다면 수동으로 다시 만들어 줄 수 있으며 이는 처음 constructor
의 특징을 그대로 보장한다
Rabbit.prototype = {
jumps: true,
constructor: Rabbit
};
*References
Author And Source
이 문제에 관하여(프로토타입과 프로토타입 상속), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@ayaan92/프로토타입과-프로토타입-상속저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)