상속과 프로토타입
상속은 객체지향 프로그래밍의 핵심 개념으로, 어떤 객체의 프로퍼티 또는 메서드를 다른 객체가 상속받아 그대로 사용할 수 있는 것을 말한다. 자바스크립트는 프로토타입 기반으로 상속을 구현하여 불필요한 중복을 제거한다.
// 생성자 함수
function Circle(radius) {
this.radius = radius;
this.getArea = function() {
return Math.PI * this.radius ** 2;
}
}
const circle1 = new Circle(1);
const circle2 = new Circle(2);
위 코드를 보면 Circle 인스턴스를 생성할 때마다 getArea 메서드를 중복 생성하고 모든 인스턴스가 중복 소유한다. 이럴 경우 메모리를 불필요하게 낭비하고, 인스턴스를 생성할 때마다 메서드를 생성하므로 퍼포먼스에도 악영향을 준다. getArea 메서드를 하나만 생성하여 모든 인스턴스가 공유하는 방법은 뭐가 있을까?
// 생성자 함수
function Circle(radius) {
this.radius = radius;
}
// Circle 생성자 함수가 생성한 모든 인스턴스가 getArea 메서드를 공유하도록 prototype에 추가한다.
Circle.prototype.getArea = function() {
return Math.PI * this.radius ** 2;
};
const circle1 = new Circle(1);
const circle2 = new Circle(2);
console.log(circle1.getArea === circle2.getArea); // true
바로 prototype 프로퍼티를 이용하는 것이다. prototype 프로퍼티는 생성자 함수로 호출할 수 있는 함수 객체, 즉 constructor 만이 소유하는 프로퍼티다. 일반 객체와 생성자 함수로 호출할 수 없는 (화살표 함수) non-constructor 에는 prototype 프로퍼티가 없다.
프로토타입 객체
프로토타입 객체는 객체 간 상속을 구현하기 위해 사용된다. 어떤 객체의 부모 역할을 하는 객체로서 다른 객체에 공유 프로퍼티를 제공한다. 프로토타입을 상속받은 자식 객체는 상위 객체의 프로퍼티를 자신의 프로퍼티처럼 자유롭게 사용할 수 있다.
모든 객체는 [[Prototype]]이라는 내부 슬롯을 가지며, 이 내부 슬롯의 값은 프로토타입의 참조이다. 객체가 생성될 때 객체 생성 방식에 따라 프로토타입이 결정되고 [[Prototype]]에 저장된다.
[[Prototype]] 내부 슬롯에는 직접 접근할 수는 없지만 __proto__ 접근자 프로퍼티를 통해 자신의 프로토타입 즉 [[Prototype]] 내부 슬롯이 가리키는 프로토타입에 간접적으로 접근할 수 있다.
__proto__ 접근자 프로퍼티
모든 객체는 __proto__ 접근자 프로퍼티를 통해 자신의 프로토타입에 간접적으로 접근 가능하다.
접근자 프로퍼티 자체적으로는 값 [[Value]] 프로퍼티 어트리뷰트를 갖고 있지 않고, 다른 데이터 프로퍼티의 값을 읽거나 저장할 때 사용하는 접근자 함수, 즉 [[Get]], [[Set]] 프로퍼티 어트리뷰트로 구성되어 있다. getter/setter 함수라고 불리는 접근자 함수를 통해 내부 슬롯의 값, 즉 프로토타입을 취득하거나 할당한다. __proto__ 접근자 프로퍼티를 통해 프로토타입을 접근하면 내부적으로 getter 함수가 호출된다.
__proto__ 접근자 프로퍼티는 상속을 통해 사용된다. 객체가 직접 소유하는 게 아니라 Object.prototype의 프로퍼티다.
const person = {name: "Kang"};
// person 객체는 __proto__ 프로퍼티를 소유하지 않는다.
console.log(person.hasOwnProperty('__proto__')); // false
// 모든 객체는 Object.prototype의 접근자 프로퍼티 __proto__를 상속받아 사용할 수 있다.
console.log({}.__proto__ === Object.__proto__); // true
__proto__ 접근자 프로퍼티를 코드 내에서 직접 사용하는 것은 권장하지 않는다.
모든 객체가 __proto__ 접근자를 사용할 수 있는 것은 아니기 때문이다. Object.prototype을 상속받지 않는 객체를 생성할 수도 있기 때문이다.
// obj는 프로토타입 체인의 종접이다. 따라서 Object.__proto__를 상속받을 수 없다.
const obj = Object.create(null);
console.log(obj.__proto__); // undefined
console.log(Object.getPrototypeOf(obj)); // null
__proto__ 접근자 프로퍼티 대신 프로토타입의 참조를 취득하고 싶은 경우에는 Object.getPrototypeOf 메서드를 사용하고, 프로퍼티를 교체하고 싶은 경우에는 Object.setPrototypeOf 메서드 사용할 것을 권장한다.
const obj = {};
const parent = {x: 1};
// obj의 프로토타입을 반환한다.
Object.getPrototypeOf(obj);
// obj의 프로토타입을 parent로 교체한다.
Object.setPrototypeOf(obj, parent);
console.log(obj.x); // 1
함수 객체의 prototype 프로퍼티
함수 객체만이 소유하는 prototype 프로퍼티는 생성자 함수가 생성할 인스턴스의 프로토타입을 가리킨다.
하지만 prototype 프로퍼티는 프로토타입 객체를 가리키는 [[Prototype]] 내부 슬롯과는 다르다는 것이다. 둘 다 모두 프로토타입 객체를 가리키지만 관점의 차이가 있다.
// 함수객체는 prototype 프로퍼티를 소유한다.
(function () {}).hasOwnProperty('prototype'); // true
// 일반 객체는 prototype 프로퍼티를 소유하지 않는다.
({}).hasOwnProperty('prototype'); // false
prototype 프로퍼티는 생성자 함수가 생성할 객체의 프로토타입을 가리킨다. 따라서 non-constructor 인 화살표 함수와 ES6 메서드 축약 표현으로 정의한 메서드는 prototype 프로퍼티를 소유하지 않고, 생성하지도 않는다.
모든 객체가 가지고 있는 (Object.prototype으로부터 상속받은) __proto__ 접근자 프로퍼티와 함수 객체만이 가지고 있는 prototype 프로퍼티는 결국 동일한 프로토타입을 가리킨다. 하지만 이들 프로퍼티를 사용하는 주체가 다르다.
- [[Prototype]] : 함수를 포함한 모든 객체가 가지고 있는 내부 슬롯이다. 객체의 입장에서는 자신의 부모 역할을 하는 프로토타입 객체를 가리키며 함수 객체의 경우 Function.prototype을 가리킨다.
- prototype 프로퍼티 : 함수 객체만 가지고 있는 프로퍼티다. 함수 객체가 생성자로 사용될 때 이 함수를 통해 생성될 객체의 부모역할을 하는 프로토타입 객체를 가리킨다.
구분 | 소유 | 값 | 사용 주체 | 사용 목적 |
__proto__ 접근자 프로퍼티 | 모든 객체 | 프로토타입의 참조 | 모든 객체 | 객체가 자신의 프로토타입에 접근 또는 교체하기위해 사용 |
prototype 프로퍼티 | constructor | 프로토타입의 참조 | 생성자 함수 | 생성자 함수가 자신이 생성할 객체의 프로토타입을 할당하기 위해 사용 |
function Person(name) {
this.name = name;
}
const me = new Person("Kang");
console.log(Person.prototype === me.__proto__); // true
// 결국 Person.prototype 과 me.__proto__ 는 동일한 프로토타입을 가리킨다.
사용자 정의 생성자 함수와 프로토타입 생성 시점
생성자 함수로서 호출할 수 있는 함수, 즉 constructor는 함수 정의가 평가되어 함수 객체를 생성하는 시점에 프로토타입도 더불어 생성된다. 하지만 생성자 함수로서 호출할 수 없는 non-constructor (화살표 함수 등)은 프로토타입이 생성되지 않는다.
const Person = name => {
this.name = name;
}
console.log(Person.prototype); // undefined
instanceof 연산자
이 연산자는 이항 연산자로서 좌변에 객체를 가리키는 식별자, 우변에 생성자 함수를 가리키는 식별자를 피연산자로 받는다. 만약 우변의 피연산자가 함수가 아닌 경우 TypeError가 발생한다.
우변의 생성자 함수의 prototype에 바인딩된 객체가 좌변 객체의 프로토타입 체인 상에 존재하면 true, 아니면 false로 평가된다.
function Person(name) {
this.name = name
}
const me = new Person('kang');
console.log(me instanceof Person); // true
console.log(me instanceof Object); // true
instanceof 연산자가 어떻게 동작하는지 보기위해 프로토타입을 교체하도록 한다.
function Person(name) {
this.name = name
}
const me = new Person('kang');
const parent = {}; // 프로토타입 교체할 객체
Object.setPropertyOf(me, parent); // 프로토타입 교체
// 현재 Person 생성자 함수와 parent 객체는 연결되어 있지 않다.
console.log(Person.prototype === parent); // false
console.log(parent.prototype === Person); // false
// me의 프로토타입이 parent로 교체되었기 때문에 me 객체의 프로토타입 체인상에 Person이 존재하지 않음
console.log(me instanceof Person); // false
// Object 프로토타입은 가지고 있다.
console.log(me instanceof Object); // true
// parent 객체를 Person 생성자 함수의 prototype으로 바인딩하면. 아래의 결과가 나타난다.
Person.prototype = parent;
console.log(me instanceof Person); // true
이처럼 instanceof 연산자는 프로토타입의 constructor 프로퍼티가 가리키는 생성자 함수를 찾는 것이 아니라 생성자 함수의 prototype에 바인딩된 객체가 프로토타입 체인 상에 존재하는지 확인한다.
직접 상속
Object.create
Object.create 메서드는 명시적으로 프로토타입을 지정하여 새로운 객체를 생성한다. 첫 번째 매개변수에는 생성할 객체의 프로토타입으로 지정할 객체를 전달하고, 두 번째 매개변수에는 생성할 객체의 프로퍼티 키와 프로퍼티 디스크립터 객체로 이뤄진 객체를 전달한다. 두 번째 매개변수는 옵션이므로 생략 가능하다.
let obj = Object.create(null); // 프로토타입이 null인 객체를 생성한다. 프로토타입 체인의 종점이다.
const myProto = { x: 10 };
obj = Object.create(myProto); // 임의의 객체 myProto를 직접 상속받는다.
console.log(obj.x); // 10
console.log(Object.getPropertyOf(obj) === myProto); // true
이 메서드의 장점은 아래와 같다.
- new 연산자 없이 객체를 생성할 수 있다.
- 프로토타입을 지정하면서 객체를 생성할 수 있다.
- 객체 리터럴에 의해 생성된 객체도 상속받을 수 있다.
하지만 Object.prototype의 빌트인 메서드를 객체가 직접 호출하는것을 권장하지 않는데, 그 이유는 Object.create를 통해 프로토타입 체인 종점에 위치하는 객체를 생성할 수 있기 때문이다.
const obj = Object.create(null); // 프로토타입 체인의 종점에 위치하는 객체
obj.a = 1;
console.log(Object.getPropertyOf(obj) === null); // true
// obj는 Object.prototype의 빌트인 메서드를 사용할 수 없다.
console.log(obj.hasOwnProperty('a')); // TypeError: obj.hasOwnProperty not a function
// 그러기 때문에 Object.prototype의 빌트인 메서드는 간접적으로 호출하는것이 좋다.
console.log(Object.prototype.hasOwnProperty.call(obj, 'a')); // true
__proto__ 에 의한 직접상속
Object.create는 여러 장점도 있지만, 두 번째 인자로 프로퍼티를 정의하는 것은 번거롭다. __proto__ 접근자 프로퍼티로도 직접 상속을 구현할 수 있다.
const myProto = { x: 20 };
const obj = {
y: 20,
// 객체를 직접 상속받는다
// obj -> myProto -> Object.prototype -> null
__proto__: myProto
}
console.log(obj.x, obj.y); // 10 20
console.log(Object.getPrototypeOf(obj) === myProto); // true
정적 프로퍼티/메서드
정적(static) 프로퍼티/메서드는 생성자 함수로 인스턴스를 생성하지 않아도 참조/호출할 수 있는 프로퍼티/메서드를 말한다. 생성자 함수에 추가한 정적 프로퍼티/메서드는 생성자 함수로 참조/호출할 수 있다. 하지만 생성자 함수로 생성한 인스턴스로는 참조/호출할 수 없다.
생성자 함수가 생성한 인스턴스는 자신의 프로토타입 체인에 속한 객체의 프로퍼티/메서드에 접근할 수 있다. 하지만 정적 프로퍼티/메서드는 인스턴스의 프로토타입 체인에 속한 객체의 프로퍼티/메서드가 아니므로 접근 불가능하다.
'Javascript' 카테고리의 다른 글
this (0) | 2020.11.18 |
---|---|
빌트인 객체 (0) | 2020.11.18 |
생성자 함수에 의한 객체 생성 (0) | 2020.11.16 |
객체 변경 방지 (0) | 2020.11.16 |
변수 호이스팅 (0) | 2020.11.16 |
댓글