[코어 자바스크립트] 프로토타입
프로토타입 개념 이해
1. constructor, prototype, instance
저자 정재남선생님께서, 프로토타입을 다음 그림과 같이 하나의 도식으로 추상화를 하였는데, 이 단순한 추상화는 프로토타입의 개념을 압축한다. 아래의 코드 내용을 다음과 같이 도식화한 내용이다.
var instance = new Constructor();
왼쪽 꼭지점에는 Constructor(생성자 함수)를, 오른쪽 꼭짓점에는 Constructor.prototype이라는 프로퍼티를 위치시켰다. 왼쪽 꼭짓점으로부터 아래를 향한 화살표 중간에 new가 있고, 화살표의 종점에는 instance가 있다. 오른쪽 꼭짓점으로부터 대각선 아래로 향하는 화살표의 종점에는 instance.__proto__이라는 프로퍼티를 위치시켰다. 자 흐름을 다시한번 살펴보자.
1. 어떤 생성자 함수(Constructor)를 new 연산자와 함께 호출하면
2. Constructor에서 정의된 내용을 바탕으로 새로운 인스턴스(Instance)가 생성된다.
3. 이때 instance에는 __proto__라는 프로퍼티가 자동으로 부여되는데,
이 프로퍼티는 Constructor의 prototype이라는 프로퍼티를 참조한다.
프로토타입의 개념을 좀 더 상세히 설명하면 이렇다.
자바스크립트는 함수에 자동으로 객체인 prototype 프로퍼티를 생성해 놓는데 해당 함수를 생성자 함수로서 사용할 경우, 즉 new 연산자와 함께 함수를 호출할 경우 그로부터 생성된 인스턴스에는 숨겨진 프로퍼티인 __proto__가 자동으로 생성되며, 이 프로퍼티는 생성자 함수의 prototype 프로퍼티를 참조한다. __proto__ 프로퍼티는 생략가능하도록 구현돼 있기 떄문에 생성자 함수의 prototype에 어떤 메서드나 프로퍼티가 있다면 인스턴스에서도 마치 자신의 것처럼 해당 메서드나 프로퍼티에 접근할 수 있다.
예를 들어서, Person 이라는 생성자 함수의 prototype에 getName이라는 메서드를 지정했다고 해보자.
var Person = function (name) {
this._name = name;
}
Person prototype.getName = function() {
return this._name;
}
이제, Person의 인스턴스는 __proto__ 프로퍼티를 통해 getName을 호출할 수 있다.
왜냐하면 instance의 __proto__가 Constructor의 prototype 프로퍼티를 참조하므로 결국 둘은 같은 객체를 바라보기 때문
var suzi = new Person('Suzi');
suzi.__proto__.getName() // undefined
오잉? undefined..? 어렵다. instance의 __proto__는 Constructor의 prototype을 참조하지 않는가..? 비교해보자.
Person.prototype === suzi.__proto__ // true
역시 참조한다. 같다. 그럼 왜 'undefined' 값이 나왔을까?
'Suzi'라는 값이 나오지 않았다는 것보다, '에러가 발생하지 않았다'는 점이 중요하다. 어떤 변수를 실행해 에러가 발생하지 않고 'undefined'가 나왔다는 것은 이 변수를 호출할 수 있다는 것이다.
그렇다면, 원하는 'Suzi'가 나오지 않은 이유는 바로 this에 바인딩된 대상이 잘못 지정됐기 때문이다.
어떤 함수를 '메서드로서' 호출할 때는 메서드명 바로 앞의 객체가 곧 this가 된다. 그러니까 suzi.__proto__.getName() 에서 getName 함수 내부에서의 this는 suzi.__proto__라는 객체가 되는 것이다. 이 객체 내부에는 name 프로퍼티가 없으므로 '찾고자 하는 식별자가 정의돼 있지 않을 때는 Error 대신 undefined를 반환한다.' 라는 자바스크립트 규약에 의해 undefined가 반환됐다.
그렇다면, suzi.__proto__.getName()을 통해서 의도한 값을 얻고 싶다면, __proto__ 객체에 name 프로퍼티가 있으면 되지 않을까?
var suzi = new Person('Suzi');
suzi.__proto__.name = 'Suzi';
suzi.__proto__.getName() = // Suzi;
예상대로 잘 출력된다. 그러니까 관건은 this이다.
그렇다면, __proto__ 없이 this를 인스턴스로 하여 곧바로 메서드를 쓰는건 어떨까?
var suzi = new Person('Suzi', 28);
suzi.getName(); // Suzi
이번에도... 띠용? __proto__를 빼면 this는 instance를 가리키는게 맞지만, 이대로 호출되고 심지어 원하는 값이 나오는게 정상적일까? 혼란의 여지가 있다. 하지만, 정상입니다. 그 이유는 바로 __proto__가 생략가능한 프로퍼티이기 때문이다.
자, 이제 프로토타입의 도식을 살짝 바꿔 정의해본다.
new 연산자로 Constructor를 호출하면 instance가 만들어지는데, 이 instance의 생략 가능한 프로퍼티인 __proto__는 Constructor의 prototype을 참조한다.
정리
어떤 생성자 함수를 new 연산자와 함께 호출하면 Constructor에서 정의된 내용을 바탕으로 새로운 인스턴스가 생성되는데, 이 인스턴스에는 __proto__라는 Constructor의 prototype 프로퍼티를 참조하는 프로퍼티가 자동으로 부여된다. __proto__는 생략 가능한 속성이라서, 인스턴스는 Constructor.prototype의 메서드를 마치 자신의 메서드인 것처럼 호출할 수 있다.
Constructor.prototype에는 constructor라는 프로퍼티가 있는데, 이는 다시 생성자 함수 자신을 가리킵니다. 이 프로퍼티는 인스턴스가 자신의 생성자 함수가 무엇인지를 알고자할 때 필요한 수단이다.
직각삼각형의 대각선 방향, 즉 __proto__ 방향을 계속 찾아가면 최종적으로는 Object.prototype에 당도하게 된다. 이런식으로 __proto__ 를 찾아가는 과정을 프로토타입 체이닝이라고 하며, 이 프로토타입 체이닝을 통해 각 프로토타입 메서드를 자신의 것처럼 호출할 수 있다. 이때 접근 방식은 자신으로부터 가장 가까운 대상으로 부터 점차 먼 대상으로 나아가며 원하는 값을 찾으면 검색을 중단한다.
Object.prototype에는 모든 데이터 타입에서 사용할 수 있는 범용적인 메서드만이 존재하며, 객체 전용 메서드는 여느 데이터 타입과 달리 Object 생성자 함수에 스태틱하게 담겨있다.
프로토타입 체인은 반드시 2단계로만 이뤄지는 것이 아니라 무한대의 단계를 생성할 수도 있다.