본문으로 바로가기

6장 프로토타입

category 코어 자바스크립트 2020. 12. 24. 16:39

자바스크립트는 프로토타입 기반 언어이다, 클래스 기반 언어에서는 '상속'을 사용하지만 프로토타입 기반 언어에서는 어떤 객체를 원형(prototype)으로 삼고 이를 복제(참조)함으로써 상속과 비슷한 효과를 얻는다

 

프로토타입의 개념 이해

constructor, prototype, instance

var instance = new Constructor();
  • 어떤 생성자함수를 new 연산자와 함께 호출하면
  • Constructor에서 정의된 내용을 바탕으로 새로운 인스턴스가 생성된다
  • 이때 instance에는 __proto__라는 프로퍼티가 자동으로 부여되는데,
  • 이 프로퍼티는 Constructor의 prototype이라는 프로퍼티를 참조한다

prototype과 __proto__의 관계가 프로토타입 개념의 핵심이다

prototype은 객체이다, 이를 참조하는 __proto__역시 객체이다

prototype객체 내부에는 인스턴스가 사용할 메서드를 저장하는데 그러면 인스턴스에서도 숨겨진 프로퍼티인 __proto__를 통해 이 메서드들에 접근할 수 있게 된다

 

예제) Person.prototype 

var Person = function (name) {
    this._name = name;
};
Person.prototype.getName = function () {
    return this._name;
};

var suzi = new Person('Suzi');
suzi.__proto__.getName();   // undefined

Person의 인스턴스 suzi는 __proto__를 통해 getName을 호출할 수 있다

왜냐하면 instance의 __proto__가 Constructor의 prototype 프로퍼티를 참조하므로 결국 둘은 같은 객체를 바라보기 때문이다

 

Person.prototype === suzi.__proto__   // true

위의 예제에서 메서드 호출 결과로 undefined가 나온 점에 주목해보면 'Suzi'라는 값이 나오지 않은 것보다는 '에러가 발생하지 않았다'는 점이 우선이다

어떤 변수를 실행해 undefined가 나왔다는 것은 이 변수가 '호출할 수 있는 함수'에 해당한다는 것을 의미한다

만약 실행할 수 없는, 즉 함수가 아닌 다른 데이터 타입이었다면 TypeError가 발생했을 것이다

그런데 값이 에러가 아닌 다른 값이 나왔으니까 getName이 실제로 실행됐음을 알 수 있고, 이로부터 getName이 함수라는 것이 입증됐다

 

다만 값이 undefined로 나온 이유는 this에 바인딩된 대상이 잘못되었기 때문이다

어떤 함수를 메서드로서 호출할 때는 메서드명 바로 앞의 객체가 곧 this가 되는데 suzi.__proto__.getName() 에서 getName 함수 내부에서의 this는 suzi가 아니라 suzi.__proto__라는 객체가 되는 것이다

이 객체 내부에는 name 프로퍼티가 없으므로 '찾고자 하는 식별자가 정의돼 있지 않을 때는 Error 대신 undefined를 반환한다'라는 자바스크립트 규약에 의해 undefined가 반환된 것이다

 

그럼 만약 __proto__객체에 name프로퍼티가 있다면 어떨까?

var suzi = new Person('Suzi');
suzi.__proto__.name = 'SUZI__proto__';
suzi.__proto__.getName();  // SUZI__proto__

예상대로 SUZI__proto__ 가 잘 출력된다, 그러니까 관건은 this인 것이다

그래서 this를 인스턴스로 하려면 __proto__ 없이 인스턴스에서 곧바로 메서드를 쓰면 된다

var suzi = new Person('Suzi', 28);
suzi.getName();  // Suzi
var iu = new Person('Jieun', 28);
iu.getName();  // Jieun

__proto__를 빼면 this는 instance가 되는 게 맞지만 이대로 메서드가 호출되고 심지어 원하는 값이나오는 것은 좀 이상하게 느껴지는데 그 이유는 바로 __proto__가 생략 가능한 프로퍼티이기 때문이다

프로토 타입의 핵심은 'new 연산자로 Constructor를 호출하면 instance가 만들어지는데, 이 instance의 생략 가능한 프로퍼티인 __proto__는 Constructor의 prototype을 참조한다' 라는 것이다

 

프로토 타입의 개념을 좀 더 상세히 설명하자면 자바스크립트는 함수에 자동으로 객체인 prototype 프로퍼티를 생성해 놓는데, 해당 함수를 생성자 함수로서 사용할 경우, 즉 new 연산자와 함께 함수를 호출할 경우, 그로부터 생성된 인스턴스에는 숨겨진 프로퍼티인 __proto__가 자동으로 생성되며, 이 프로퍼티는 생성자 함수의 prototype 프로퍼티를 참조한다, __proto__ 프로퍼티는 생략 가능하도록 구현돼 있기 때문에 생성자 함수의 prototype에 어떤 메서드나 프로퍼티가 있다면 인스턴스에서도 마치 자신의 것처럼 해당 메서드나 프로퍼티에 접근할 수 있게 된다

 

 

프로토타입 체인

메서드 오버라이드

원본이 있는 상태의 메서드 위에 덮어 씌우는 메서드를 메서드 오버라이드라고 한다

var Person = function (name) {
    this.name = name;
};
Person.prototype.getName = function () {
    return this.name;
};

var iu = new Person('지금');
iu.getName = function () {
    return '바로 ' + this.name;
};
console.log(iu.getName());  // 바로 지금

자바스크립트 엔진이 getName이라는 메서드를 찾는 방식은 가장 가까운 대상인 자신의 프로퍼티를 검색하고, 없으면 그 다음으로 가까운 대상인 __proto__를 검색하는 순서로 진행된다

그래서 인스턴스인 iu에 같은 이름의 getName()을 추가하면 iu의 프로퍼티인 getName()이 호출된다

 

그러면 원본인 prototype에 있는 메서드에 접근이 불가능 할까? 라는 의문이 드는데

console.log(iu.__proto__.getName());  // undefined

이 상황은 아까 __proto__가 name이라는 프로퍼티가 없어서 undefined가 나왔다고 했다

그래서 name을 추가해주면 된다

Person.prototype.name = '이지금';
console.log(iu.__proto__.getName()); // 이지금

이렇게 메서드 오버라이딩한 메서드에서도 원본(prototype)에 있는 메서드를 호출 할 수 있다는 결과가 나왔다

 

다만 위의 코드는 this가 prototype을 바라보고 있는데 이걸 인스턴스를 바라보게 바꿔주면 될 것이다

this가 바라보는 대상을 지정할때 사용하는 메서드 call/apply를 사용하면 될 것이다

console.log(iu.__proto__.getName.call(iu));  // 지금

이런식으로 메서드가 오버라이딩된 경우에는 자신이 가장 가까운 메서드에만 접근할 수 있지만, 그다음으로 가까운 __proto__의 메서드도 우회적인 방법을 통해서 접근이 불가능한 것은 아니다

 

 

프로토타입 체인

 

객체의 내부구조

console.dir({ a: 1 });

 

객체의 내부구조를 살펴보면 __proto__ 내부에 다양한 메서드들을 포함하고 있고 constructor는 생성자 함수인 Object를 가리키고 있다

 

배열의 내부구조

console.dir([1]);

배열을 살펴보면 __proto__내부에 constructor는 생성자 함수인 Array를 가리키고 있는데

Array.prototpye의 __proto__는 Object를 가리키고 있다

__proto__는 생략 가능하니 배열은 Array.prototype의 내부의 메서드를 마치 자신의 것처럼 실행할 수 있고

마찬가지로 생략가능한 __proto__를 한 번 더 따라가면 Object.prototype의 메서드도 자신의 것처럼 실행할 수 있다

 

프로토타입 체이닝은 위의 메서드 오버라이드와 동일한 맥락으로 어떤 메서드를 호출하면 자바스크립트 엔진은 데이터 자신의 프로퍼티들을 검색해서 원하는 메서드가 있으면 그 메서드를 실행하고, 없으면 __proto__를 검색해서 있으면 그 메서드를 실행하고, 없으면 다시 __proto__를 검색해서 실행하는 식으로 진행한다

 

 

객체 전용 메서드의 예외사항

어떤 생성자 함수이든 prototype은 반드시 객체이기 때문에 Object.prototype이 언제나 프로토타입 체인의 최상단에 존재하게 된다

따라서 객체에서만 사용할 메서드는 다른 여느 데이터 타입처럼 프로토타입 객체 안에 정의할 수가 없다

객체에서만 사용할 메서드를 Object.prototype 내부에 정의한다면 다른 데이터 타입도 해당 메서드를 사용할 수 있기 때문이다

 

이 같은 이유로 객체만을 대상으로 동작하는 객체 전용 메서드들은 부득이 Object.prototype이 아닌 Object에 스태틱 메서드로 부여할 수 밖에 없었다

또한 생성자 함수인 Object와 인스턴스인 객체 리터럴 사이에는 this를 통한 연결이 불가능하기 때문에 여느 전용 메서드처럼 '메서드명 앞의 대상이 곧 this'가 되는 방식 대신 this의 사용을 포기하고 대상 인스턴스를 인자로 직접 주입 해야하는 방식으로 구현돼 있다

 

Object전용메서드

객체 한정 메서드중 freeze라는 메서드가

Object.freeze(instance)가 아닌 instance.freeze()처럼 표현 할 수 있었다면

참조형 데이터뿐 아니라 기본형 데이터조차 __proto__에 접근하여 freeze()를 사용할 수 있었을 것이다

그래서 자바스크립트는 예외적으로 prototype 내부에 정의하지 않은 메서드들이 있는 것이다

 

 

다중 프로토타입 체인

자바스크립트의 기본 내장 데이터 타입들은 모두 프로토타입 체인이 1단계(객체) 이거나 2단계(나머지)로 끝나는 경우만 있었지만 사용자가 새롭게 만드는 경우에는 그 이상도 얼마든지 가능하다 __proto__를 연결해 나가기만 하면 무한대로 체인 관계를 이어나갈 수 있다

var Grade = function () {
    var args = Array.prototype.slice.call(arguments);
    for(var i = 0; i < arg.length; i++){
        this[i] = args[i];
    }
    this.length = args.length;
};
var g = new Grade(100, 80);

변수 g는 배열의 형태를 지니지만, 배열의 메서드는 사용할 수는 없는 유사배열 객체이다

유사배열객체에 배열 메서드를 적용하는 방법으로 call/apply를 사용할 수 있지만

이번에는 인스턴스에서 배열 메서드를 직접 쓸 수 있게끔 해보자

 

그러기 위해서는 g.__proto__, 즉 Grade.prototype이 배열의 인스턴스를 바라보게 하면된다

Grade.prototype = [];

이 명령에 의해 서로 별개로 분리돼 있던 데이터가 연결되어 하나의 프로토타입 체인 형태를 띄게 된다

 

이제는 Grade의 인스턴스인 g에서 직접 배열의 메서드를 사용할 수 있다

console.log(g);  //Grade(2) [100, 80];
g.pop();
console.log(g);  //Grade(1) [100];
g.push(90);
console.log(g);  //Grade(2) [100, 90];

g인스턴스의 입장에서는 프로토타입 체인에 따라 g 객체 자신이 지니는 멤버, Grade의 prototype에 있는 멤버, Array.prototype에 있는 멤버, 끝으로 Object.prototype에 있는 멤버에까지 접근할 수 있게 됬다

'코어 자바스크립트' 카테고리의 다른 글

7장 클래스  (0) 2020.12.28
5장 클로저  (0) 2020.12.23
4장 콜백함수  (0) 2020.12.22
3장 this  (2) 2020.12.21
1장 메모리와 데이터 타입  (0) 2020.12.21
2장 실행 컨텍스트  (0) 2020.12.21