프로토타입
Javascript는 프로토타입 기반 언어이다. ES2015부터 지원하는 클래스 문법이 있지만, 내부적으로는 프로토타입을 기반으로 동작한다.
목차
프로토타입이란?
각각의 객체가 가지고 있는 [[Prototype]]이라는 private 속성을 의미한다.
이는 자신의 프로토타입이 되는 다른 객체를 가리킨다.
이러한 프로토타입은 객체를 상속받기 위한 (내부의 속성이나 메서드까지) 일종의 템플릿의 역할을 한다.
하나의 객체를 생성해보자.
let obj1 = {
name: 'Peter',
age: 20,
say(){
return '안녕';
}
}
console.log(obj1.__proto__)
여기서 __proto__ 속성은 해당 객체의 프로토타입을 가리키고 있다.
즉 obj1.__proto__는 obj1 객체의 프로토타입을 의미한다.
__proto__는 모든 일반객체가 가지고 있다. MDN에 따르면 Deprecated 된 속성이다. Object.getPrototypeOf() 로 대체되었음.
그렇다면 obj1의 프로토타입은 무엇일까?
상속받지 않은 일반 객체의 경우 자바스크립트의 최상위 객체인 Object 를 가리키게 된다.
실제로는 Object를 가리키는게 아닌 Object.prototype을 가리키고 있다.
프로토타입 체이닝
위의 obj1 객체가 갖고 있지 않은 메서드를 실행해보자.
obj1.toString()
obj1에는 toString이라는 메서드가 없으니 에러를 발생하지 않을까~?
싶겠지만 그렇지 않다.
obj1에는 toString 메서드가 존재하지 않지만 Object의 프로토타입에 toString이 존재한다.
이렇게 상위 프로토타입으로 계속 탐색해나가는 행위를 프로토타입 체이닝 이라고 한다.
객체 2개를 생성해보자.
let obj2 = {
name: 'Alice',
age: 21,
eat() {
return '쩝쩝';
}
}
let obj3 = {
name: 'Tom',
age: 23,
move() {
return '슝슝';
}
}
두 객체 모두 __proto__를 가지고 있고 Object의 prototype을 가리키고 있다.
한 번 obj2의 __proto__가 obj3을 가리키도록 해보자.
obj2.__proto__ = obj3;
console.log(obj2.eat()); // '쩝쩝'
console.log(obj2.move()); // '슝슝'
obj2에는 move라는 메서드가 없기 때문에 obj2의 프로토타입을 참조하게 된다.
이 때 obj2의 프로토타입은 obj3이므로 obj3에서 move라는 메서드를 발견하고 실행하게 되는 것이다.
console.log(obj2.foo); // undefined
프로토타입 체이닝을 통해 연결된 최상위 프로토타입까지 탐색한 뒤에 찾을 수 없다면 그 때 undefined 를 출력한다.
아까 최상위 객체가 Object라고 했는데, 이 Object의 프로토타입은 null이다.
즉, 프로토타입 체이닝 중 null이 나타난다면 그 때 해당 속성/메서드는 최종적으로 undefined가 되는 것이다.
함수의 프로토타입
javascript에서 객체를 생성하는 방법은 대충 3가지 정도 되는데
앞에서 언급한 생성방법은 객체 리터럴을 사용해서 생성하는 방법이다.
- 객체 리터럴
- Object.create()
- 생성자 함수
다른 방법 중 하나인 생성자 함수를 사용해서 객체를 생성해보자.
function Parent() {
this.name = 'Donald',
this.sayHello = function() {
return `Hello, I'm ${this.name}`;
}
}
이러한 함수 객체와 일반 객체의 차이점은 prototype이라는 속성을 갖는 것이다.
console.log(Parent.prototype);
prototype 속성은 생성자의 속성으로 __proto__와는 다르다는 것을 확실히 이해해야한다.
우선, Parent 생성자를 이용해 parent라는 인스턴스를 만들어보자.
const parent = new Parent();
// parent.__proto__ === Parent.prototype
Parent 생성자로부터 생성된 인스턴스 parent의 __proto__ 는 Parent.prototype과 같다.
즉, prototype 은 생성자의 속성으로 상속하고자 할 속성이나 메소드들을 가지고 있다.
프로토타입을 활용한 상속
프로토타입에 새로운 메서드를 추가해보자.
Parent.prototype.whoru = function() {
return this;
}
parent.whoru(); // Parent {}
그럼 함수 내부에 정의하는 것과 prototype에 정의하는 것이 무슨 차이일까?
아래와 같이 Child라는 객체를 생성해보자.
function Child() {
}
Parent 객체를 Child에서 상속받고 싶다고하자.
Child의 prototype이 Parent의 prototype이 되어야할 것이다.
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
// Child의 생성자를 설정해준다.
이를 기반으로 Child의 인스턴스를 생성해보자.
const child = new Child();
console.log(child.name); // undefined
console.log(child.sayHello()) // ERR: not a function
console.log(child.whoru()) // CC {}
어라? 왜 name과 sayHello에 접근할 수 없을까?
분명 Parent에 name과 sayHello가 정의되어있지 않았는가!
!!
그 이유는 name과 sayHello는 Parent 자체에 정의된 속성이지, prototype에 정의된 속성이 아니기 때문이다. 그래서 child.whoru()는 올바르게 동작한다.
그럼 어떻게 해야하나요? 접근할 수 없나요?
function Child() {
Parent.call(this); // Parent 생성자 호출
}
상속받고자 하는 객체의 생성자를 호출해주면 된다.
const child = new Child();
console.log(child.name); // 'Donald'
console.log(child.sayHello()) // 'Hello, I'm Donald'
console.log(child.whoru()) // Child {}