[자바스크립트] prototype.js와 객체지향

류명운

·

2016. 8. 16. 23:50

반응형

[자바스크립트] prototype.js와 객체지향


prototype js와 객체지향

prototype js 프레임 워크를 살펴보기에 앞서서 먼저 자바스크립트의 사상에 대해 살펴보고 넘어가야 한다.

가장 기본적인 프로퍼티 prototype, _proto_, constructor,.prototype, constructor 이 4가지에 대해 살펴보자

객체지향의 기본 개념인 상속(inheritance)을 이용해 어떻게 서브클래스와 부모클래스의 관계를 맺어주는지 살펴보자.



prototype, constructor, constructor.prototype, _proto_

모든 함수에는 미리 정의된 prototype 객체를 가르키는 prototype 프로퍼티가 있다.

이 prototype 객체는 함수가 new 연산자를 통해 생성자 함수로 사용될 때, 새 객체를 정의하는 과정에서 중요한 역할을 하게 된다.

코드 A

var Parent1 = function(name, age) {
 this.name = name;
 this.age  = age; 
 this.getInfo = function() { return "이름 : " + this.name + "   나이 : " + this.age ; }
}
var test1 = new Parent1('하세가와','20');
var test2 = new Parent1('하세가와','20');
var test3 = new Parent1('하세가와','20');

alert(test1.getInfo());
alert(test2.getInfo());
alert(test3.getInfo());

코드 B

var Parent2 = function(name, age) {
 this.name = name;
 this.age  = age; 
}
Parent2.prototype.getInfo = function() {
 return "이름 : " + this.name + "   나이 : " + this.age
}

var test4 = new Parent2('하세가와','20');
var test5 = new Parent2('하세가와','20');
var test6 = new Parent2('하세가와','20');

alert(test4.getInfo());
alert(test5.getInfo());
alert(test6.getInfo());

코드 A와 코드B는 결과적으로 같은 정보를 리턴한다. 하지만 메모리 관리 상에서 prototype를 이용한 방법은 좀 더 영리하게 동작한다.

생성자 함수 Parent1과 Parent2의 인스턴스를 만들어 낼 경우에 getInfo라는 프로퍼티는 Parent1 쪽에선 객체마다 생성이 되어진다(여러 브라우저의 내장된 디버그 툴을 이용해서 확인해 보자) 하지만 Parent2의 경우에는 각각 생성자 함수를 통해 만들어진 인스턴스들이 prototype된 getInfo 프로퍼티의 레퍼런스를 공유하고 있다. 그래서 메모리를 보다 아껴서 사용할 수 있다.

물론 단순히 메모리 처리만을 위해 prototype이 사용되지는 않는다. 위 예제 봤듯이 prototype된 프로퍼티는 다른 OOP 언어에서처럼 모든 객체가 공유하는 정보 즉, static 처리가 가능하다는 것을 알 수 있다.


prototype

prototype 객체는 OOP의 상속(inheritance)과 관계된 중요한 객체이다.

prototype는 다른 OOP언어에서는 클래스라고 볼수 있는 생성자 함수를 통해 인스턴스를 생성할 경우 모든 인스턴스가 공통적으로 물려받을 수 있는 코드를 생성할 수 있게 도와준다.

prototype는 생성자 함수를 정의할 때 자동적으로 생성되는 특별한 객체로서 클래스 전체의 의미가 있는 프로퍼티와 값을 담을 수 있다.

prototype이 만들어지는 과정을 살펴보면 다음과 같다.

1) 생성자 함수가 만들어지면 인터프리터는 자동으로 그 생성자 함수에 prototype 프로퍼티를 추가하게 된다 (추가된 prototype 프로퍼티에는 범용 객체를 넣어두게 된다)

2) prototype 객체에 들어가는 프로퍼티와 메서드는 그 생성자 함수로 정의된 클래스의 모든 인스턴스에 그대로 상속(반영)되게 된다.

이렇듯 해당 클래스의 prototype 객체에 추가된 프로퍼티들은 모든 인스턴스에 그대로 반영된다.

인스턴스가 프로퍼티를 가지고 있지 않아도 생성자 함수의 prototype 객체에 들어있는 프로퍼티를 인스턴스가 가지고 있는 것처럼 처리하게 된다.

생성자 함수의 모든 객체에 상속될 메서드나 프로퍼티를 만들려면 그 생성자 함수에 정의된 자동으로 미리 만들어진 프로토타입 객체에 속성으로 추가하면 된다. 만일 인스턴스에서 프로퍼티나 메서드를 찾으라고 하면 인터프리터는 기본적으로 인스턴스 차원(instance level)에서 먼저 프로퍼티나 메서드를 찾게 되는 것이다. 그 곳에 프로퍼티나 메서드가 없다면 다음은 해당 인스턴스의 원래 클래스 즉 생성자 함수의 prototype 객체에서 프로퍼티나 메서드를 찾게 되는 것이다. 만일 상속된 클래스 즉 생성자 함수 였다면 그 위의 슈퍼클래스(생성자 함수)의 prototype 객체를 다시 찾아 계속 거슬러 올라가게 되는 것이다.

자식 클래스(생성자 함수)의 인스턴스는 자신을 생성해준 엄마 클래스(생성자 함수)의 prototype으로부터 메서드와 프로퍼티를 상속받게 되는 것이다.

//인스턴스 마다 동일한 특징을 가질 수 있게 생성자 함수를 만드는 예

Cs = function(name){
  this.name = name;
  this.Nick = 'daejang'
}
Cs.prototype.Nature = 'Pure';
inst_Cs = new Cs ('나이스가이');
inst_Cs2 = new Cs ('나이스가이2');

alert(inst_Cs.name);    //1
alert(inst_Cs.Nature);  //2
alert(inst_Cs.Nick);    //3
alert(inst_Cs2.name);    //4
alert(inst_Cs2.Nature);  //5
alert(inst_Cs2.Nick);    //6


constructor

prototype 객체의 constructor 프로퍼티

생성자 함수의 prototype 객체가 만들어지고 나면 인터프리터에서는 그 객체에 자동으로 constructor라는 특별한 프로퍼티를 추가하게 된다.

Constructor 프로퍼티는 prototype 클래스 생성자 함수에 대한 레퍼런스이다.

Cs = function(name){
this.name = name;
this.Nick = 'daejang'
}
Cs.prototype.Nature = 'Pure';
inst_Cs = new Cs ('나이스가이');
inst_Cs2 = new Cs ('나이스가이2');

alert(Cs);
alert(Cs.prototype.constructor);
alert(inst_Cs.Nature);
alert(inst_Cs.prototype.Nature);
alert(inst_Cs.prototype.constructor);  // ????
alert(inst_Cs2.prototype.constructor); // ????

일반적으로 책에 나와 있는 constructor에 대한 설명은 위 예제의 범주를 벗어나지 못한다. 하지만 저 설명을 그대로 믿고 코드를 타이핑해 보면 잘못된 결과에 직면하게 된다. 즉 에러가 나온다는 말이다.


그렇다면 위 내용은 어떻게 받아들여야 하는 것일까?

위 코드는 ECMA 표준에서부터 출발해야 이해할 수 있는 코드이다.


생성자 함수로부터 객체가 생성되는 순간을 살펴보자.

inst_Cs = new Cs('나이스가이');

inst_Cs2 = new Cs('나이스가이2');

new 키워드를 이용하여 만들어진 객체 inst_Cs와 inst_Cs2는 prototype 프로퍼티를 가지고 있지 않는다.

그럼 대체 어떻게 상속이 이루어지는 것일까?

ECNA 이전 버전에서는(물론 지금도 있다. 파이어폭스에서만..) _proto_가 존재 했었다.

이 프로퍼티의 특징은 생성자 함수를 new 키워드를 통해 생성했을 때 생성자 함수의 prototype 객체를 내부적으로 참조한다. 그리고 만들어진 객체는 prototype을 참조하고 있는 _proto_의 프로퍼티들을 객체 자신의 프로퍼티로 가지고 온다. (객체명._proto_.프로퍼티명 이렇게 접근 하던걸 객체명.프로퍼티명 이렇게 접근하게 한다)

즉 방향성으로 보았을때 prototype의 내용을 바로 가지고 오는 것이 아니라 _proto_를 통해서 상속구조를 완성하는 것이다. 즉 new로 만들어진 객체에는 prototype 프로퍼티를 직접 프로퍼티명으로 지칭하는 일이 없다. 내부적으로 _proto_를 통해서 객체 자신의 프로퍼티로 가지고 오고 있을 뿐이다.


한 마디로 정리하자면 다음과 같다.

new로 생성된 객체는 prototype 프로퍼티가 없고 _proto_ 혹은 constructor.prototype을 통해서 접근이 가능하다. (constructor.prototype은 밑에서 살펴볼 것이다)


참조 * 자바스크립트의 모든객체(생성자를 포함해서 new로 생성된 모든 객체)는 constructor.prototype(_proto_와 동일한 의미)이라고 하는 또다른 객체를 내부적으로 참조하고 있다. 그리고 객체는 constructor.prototype의 프로퍼티들을 자신의 프로퍼티로 가지고 온다. 다시말해 자바스크립트의 객체는 자신의 constructor.prototype에 있는 프로퍼티들을 상속받는다.

빈 객체가 생성이 되면, new 연산자는 해당 객체의 constructor.prototype(_proto_와 동일한 의미)을 설정한다. 이때 생성된 객체는, 자신을 만들어낸 생성자의 prototype 프로퍼티 값을 자신의 constructor.prototype으로 설정한다.

모든 함수(생성자 함수)에는 prototype이 있는데, 이것은 함수가 정의될 때 부터 자동적으로 생성되고 초기화 된다. prototype 프로퍼티의 초기값은 프로퍼티가 하나있는 객체로 지정된다. 이 한개의 프로퍼티가 바로 construcot이다. 그리고 이 constructor 프로퍼티는 prototype 프로퍼티가 연관되어 있는 생성자 함수를 가르킨다. 이러한 연관관계로 인해서 모든 객체에 constructor 프로퍼티가 존재하는 이유가 설명이 된다. 그리고 바로 이러한 이유로 인해 개발자가 생성자 함수의 prototype 프로퍼티에 추가한 프로퍼티들이 생성자를 이용하여 초기화한 모든 객체의 프로퍼티에 상속되는 이유다. * The Definitive Guide 5/E 202page


그렇다면 다음 예제를 살펴보자.

Cs = function(name){
this.name = name;
this.Nick = 'daejang'
}
Cs.prototype.Nature = 'Pure';
inst_Cs = new Cs ('나이스가이');
inst_Cs2 = new Cs ('나이스가이2');

alert(inst_Cs.name);    //1
alert(inst_Cs.Nature);  //2
alert(inst_Cs.Nick);    //3
alert(Cs);              //4
alert(Cs.prototype.constructor); //5
alert(inst_Cs);                  //6
alert(inst_Cs.Nature);           //7

Cs.prototype.Nature = 'Pure2';   //8
alert(inst_Cs.Nature);           //9
alert(Cs.__proto__);             //10
alert(inst_Cs.__proto__);        //11
alert(inst_Cs.constructor.prototype); //12
alert(inst_Cs.constructor.prototype.constructor); //13
alert(inst_Cs.constructor); //14
inst_Cs.constructor.prototype.Nature = 'Pure2'    //15
alert(inst_Cs2.constructor.prototype.Nature);     //16
alert(inst_Cs2.constructor.prototype.toString);   //17
alert(inst_Cs2.toString);                         //18
alert(Cs.toString);                               //19
alert(Cs.prototype.toString);                     //20

이 예제는 모든 것을 보여주고 있다. 내부가 어떻게 돌아가고 있는지를 말이다.

아까 _proto_를 이야기하며 파이어폭스에서만 쓰고 있다고 하였다. 이 말은 표준이 아니라는 말이다. 현재 IE와 파이어폭스 객체명._proto_를 대체해서 쓸수 있는건 객체명.constructor.prototype이다.

위 코드를 살펴보면 재미있는 내용이 많이 있다. 첫 번째로 4번과 5번 13번 14번이 왜 같은 결과를 리턴하는 것일까?


방금 전에 설명했던 내용을 적용하면 바로 설명이 가능하다.

모든 생성자 함수와 함수에는 prototype이라는 프로퍼티가 있는데 이것은 함수가 정의될 때 자동적으로 생성되고 초기화 된다. 이때 새로 생성된 객체는 자신을 만든 함수의 prototype를 객체 자신의 _proto_를 통해서 레퍼런스 한 다음에 객체 자신의 _proto_의 프로퍼티를 자신의 프로퍼티로 설정한다.

(객체명._proto_.프로퍼티명 이렇게 접근하던걸 객체명.프로퍼티명 이렇게 접근하게 한다)

이렇게 참조가 전달되는 과정에서 맨 처음 생성자 함수가 정의될 때 함수에 prototype 프로퍼티가 자동으로 생성되고 초기화 되듯이 prototype 또한 자기 자신의 프로퍼티로 constructor 프로퍼티를 가지게 된다. 이 constructor 프로퍼티는 prototype이 연관되어 있는 생성자 함수를 가르킨다. 이러한 이유로 객체가 생성될 때 prototype이 가지고 있는 프로퍼티가 _proto_로 참조되면서 constructor 역시 _proto_에 참조되어 진다.


그래서 위에 설명 한대로

객체명._proto_.프로퍼티명 → 객체명.프로퍼티명 이렇게 접근 하는걸 풀어서 보면

inst_Cs._proto_.constructor 이렇게 접근 하는것과 inst_Cs.constructor, Cs.prototype.constructor 접근 하는게 같은 결과를 나타낼 수 있는 것이다.


자바스크립트는 이러한 특성으로 인해 상속구조를 구현할 수 있는 것이다.


특정 인스턴스나 객체에서 부모에서 상속된 프로퍼티나 메서드를 새롭게 만들려면(재정의, overriding) 같은 이름의 프로퍼티나 메서드를 다시 정의하면 된다. 달리 말하면 자식 객체나 인스턴스에서 부모에서 상속된 프로퍼티나 메서드와 동일한 이름이지만 기능이 다른 것을 만들고 싶다면 단순히 이름만 같이 사용해서 새롭게 정의하면 해당 프로퍼티나 메서드는 부모와의 상속 고리가 끊어지게 된다는 것이다. 이것이 가능한 이유는 검색을 하는 순서 상 인터프리터는 해당 인스턴스에서 해당 프로퍼티나 메서드가 있으면 더 이상 부모까지 찾아가지 않기 때문이다.



각 프로퍼티의 검색 순위

클래스 즉 생성자 함수 차원의 프로퍼티와 인스턴스 즉 객체의 프로퍼티는 서로 다르다.

(prototype으로 확인 가능하다. 생성자 함수의 prototype은 해당 생성자의 인스턴스에선 prototype로 접근하지 못하고 _proto_ 혹은 constructor.prototype으로 접근해야 한다)

정확히 말하면 prototype chain이라는 상속 처럼 보이는 과정에 얽히게 되어 있을 뿐이지 서로 다른 프로퍼티라는 의미다. (동일한 물체를 가리키는 A와 B가 있다고 할때 두 A와 B가 같은걸 가르킨다고 해서 같은건 아니지 않는가?)

인스턴스 차원의 프로퍼티라는 것은 클래스를 통해 인스턴스를 생성한 당시나 이후에 변경된 인스턴스의 프로퍼티를 말한다. 앞에서 말씀드린 것처럼 인터프리터는 다음 순서로 프로퍼티를 찾게 된다.

1. instance level : 먼저 해당 인스턴스에 해당 프로퍼티가 있는지 인스턴스 차원에서 확인한다.

2. instance level(지역변수로 정의된 것) : 인스턴스를 만든 클래스의 원 코드에 정의된 프로퍼티가 있는지 확인한다.

3. prototype level : prototype 객체에서 프로퍼티가 있는지 확인한다.

    var Cs = function(){
    this.Nick = 'daejang (instance level (지역변수로 정의된 것) property)' // 2
    }
    Cs.prototype.Nick = 'daejang (prototype level property)'; // 3
    inst_Cs = new Cs ();
    inst_Cs.Nick = 'daejang (instance level property)'; // 1
    alert(inst_Cs.Nick);

위 코드의 출력결과는 'daejang (instance level property)' 이다.

    var Cs = function(){
    this.Nick = 'daejang (instance level (지역변수로 정의된 것) property) 2' // ?
    }
    Cs.prototype.Nick = 'daejang (prototype level property)'; // ?
    inst_Cs = new Cs ();
    alert(inst_Cs.Nick);

위 코드의 출력결과는 'daejang (instance level (지역변수로 정의된 것) property) 2' 이다.

    var Cs = function(){
    this.Nick = 'daejang (instance level (지역변수로 정의된 것) property)' // #2
    }
    Cs.prototype.Nick = 'daejang (prototype level property)'; // #3
    inst_Cs = new Cs ();
    inst_Cs.Nick = 'daejang (instance level property)'; // #1

    delete inst_Cs.Nick;
    alert(inst_Cs.Nick);

위 코드의 출력결과는 'daejang (prototype level property)' 이다. delete 연산자를 통해서 프로퍼티를 완전히 삭제하는걸 보여준다. 한번 코드의 흐름을 따라가 보자.

6라인을 보면 해당 시점에서 메모리 영역에 올라간 상태는 다음과 같다.

  • inst_Cs - Object
    • Nick - 'daejang (instance level (지역변수로 정의된 것) property)'
  • Cs - function()
    • prototype - Object Nick=daejang (prototype level property)
      • Nick - 'daejang (prototype level property)'

여기에서 8라인의 delete 키워드를 만나면 다음과 같이 된다.

  • inst_Cs - Object Nick=daejang (instance level property)
    • Nick - 'daejang (instance level property)'
  • Cs - function()
    • prototype - Object Nick=daejang (prototype level property)
      • Nick - 'daejang (prototype level property)'

그리고 delete 문을 모두 종료하고 나면 다음과 같은 메모리맵을 가지게 된다.

  • inst_Cs - Object Nick=daejang (prototype level property)
    • Nick - 'daejang (prototype level property)'
  • Cs - function()
    • prototype - Object Nick=daejang (prototype level property)
      • Nick - 'daejang (prototype level property)'

결과를 보자면 instance level의 프로퍼티를 제거하면 instance level (지역변수로 정의된 것)의 프로퍼리가 함께 제거되는 것을 알 수 있다. 그리고 instance level 프로퍼티를 해당 인스턴스에서 제거하면 prototype 차원의 동일명의 프로퍼티 값을 검색해준다.


* 지금까지 내용으로 알수있는 사실

자바스크립트에서의 상속은 인터프리터에 의해서 프로퍼티를 찾는 과정의 일부로서 자동으로 발생한다. 이러한 특성 때문에 예기치 못한 문제가 발생 하기도 한다.

다음 코드를 살펴보자.

var Cs = function(name){
}
inst_Cs1 = new Cs ('DragonBall');
alert(inst_Cs1.name); // 1
Cs.prototype.name = 'Daejang (prototype level property)';
inst_Cs2 = new Cs ('DrgonFly');
alert(inst_Cs1.name); // 2
alert(inst_Cs2.name);

이미 생성된 인스턴스도 뒤에 추가된 prototype 프로퍼티의 영향을 받을까?

답은 '그렇다' 이다. 자바스크립트는 인터프리터 언어이다. 그렇기 때문에 코드를 읽어 들이는 순간 순간마다 처리가 이루어진다. 위에서 살펴본 코드처럼 이미 객체가 생기고 난 다음 이더라도 다음 라인에서 코드를 읽어 들일 때 새로운 prototype 프로퍼티가 추가되면 그 다음 라인에서 읽어 들일 inst_Cs1와 inst_Cs2의 입장에서는 조건으로 주어진 프로퍼티를 자신의 객체에서 찾고 없을경우 생성자 함수의 prototype을 뒤지게 된다.



객체지향

상속

어떤 클래스를 슈퍼클래스로 만드는 것은 어떤 클래스를 서브클래로 만드는 것과 같은 말이 된다. 이런 서브 및 슈퍼클래스 관계를 맺기위해서 자바와 같은 다른 OOP 언어에서는 extends 키워드를 사용하지만 자바스크립트에서는 prototype를 사용한다. 다음과 같다.

서브클래스명(생성자함수).prototype = new 슈퍼클래스명();

var Person = function(){ // #1
}
Person.prototype.nature = 'Pure';

var Soldier = function(branch){ // #2
  this.branch = branch;
}

alert(Soldier);                       // #3
alert(Soldier.prototype.constructor); // #4

Soldier.prototype = new Person();    // #5
Soldier.prototype.title = 'Captain';

alert(Soldier);                       // #7
alert(Soldier.prototype.constructor); // #8


Soldier.prototype.constructor = Soldier; // #8

alert(Soldier);                         // #9
alert(Soldier.prototype.constructor);   // #10


Inst_Soldier1 = new Soldier('Army');
Inst_Soldier2 = new Soldier('Force');
alert(Inst_Soldier1.branch); // #11
alert(Inst_Soldier2.branch); // #12
alert(Inst_Soldier1.title); // #13
alert(Inst_Soldier2.title); // #14
alert(Inst_Soldier1.nature); // #15
alert(Inst_Soldier2.nature); // #16

생성자 체인 : 상위 생성자 함수의 생성자 함수를 명시적으로 호출하는 행위 (Soldier.prototype = new Person())


* 지금까지 내용으로 알수있는 사실

위에서 밝혔다 싶이 constructor는 생성자를 레퍼런스하는 prototype의 프로퍼티이다. 그래서 생성자 체인을 거는 5를 전후로 생성자의 값이 서로 다른 것을 확인 할수 있다. 위 코드에서는 굳이 #8 라인 처럼 contructor의 생성자를 재지정 해주는 이유가 들어나 있지 않지만 조금 뒤에 코드를 통해 저렇게 처리한 이유를 살펴보자.

결론적으로 위 코드를 통해 알수있는 내용은 부모 자식 구조의 상속 구조를 prototype의 생성자 체인을 통해 구현할 수 있다는 내용과 생성자함수.prototype.constructor는 자동으로 생성되면 동시에 자기 자신을 가르키고 있다는 점이다.


// 사각형과 정사각형의 상속 관계

function Rectangle(w, h) {
  this.width  = w;
  this.height = h;
}

Rectangle.prototype.area = function() { return this.width * this.height ; }

function PositionedRectangle(x, y, w, h) {
 Rectangle.call(this, w, h);
 this.x = x;
 this.y = y;
}

PositionedRectangle.prototype = new Rectangle();

delete PositionedRectangle.prototype.width;
delete PositionedRectangle.prototype.height;

PositionedRectangle.prototype.constructor = PositionedRectangle;

PositionedRectangle.prototype.contains = function(x, y) {
 return ( x > this.x && x < this.x + this.width &&
            y > this.y && y < this.y + this.height);
}

var r = new PositionedRectangle(2,2,2,2);
alert(r.x + ',' + r.y + ',' + r.width + ',' + r.height);
alert(r.contains(3,3));
alert(r.area());

Rectangle.call(this, w, h)과 PositionedRectangle.prototype = new Rectangle() 이 행위를 통해서 생성자 체인을 보여주었는데 이걸 좀 더 간소화한 다음 코드를 보자

//변경된 코드
function Rectangle(w, h) {
  this.width  = w;
  this.height = h;
}

Rectangle.prototype.area = function() { return this.width * this.height ; }
PositionedRectangle.prototype.superclass = Rectangle;

function PositionedRectangle(x, y, w, h) {
 this.superclass(w, h);
 this.x = x;
 this.y = y;
}

위와 같은 처리는 더 이상 생성자 체인을 구현하기 위해 call 이나 apply를 사용할 필요가 없음을 보여준다.


생성자 체인에 의한 상속의 특이점

* 서브클래스를 만든 슈퍼클래스(부모클래스)의 prototype 프로퍼티를 변경하면?

var Person = function(){
}
Person.prototype.nature = 'Pure';
var Soldier = function(branch){
this.branch = branch;
}
Soldier.prototype = new Person(); // #1
Soldier.prototype.title = 'Captain';
Inst_Soldier1 = new Soldier('Army');
Inst_Soldier2 = new Soldier('Force');

Person.prototype.nature = 'Very Pure'; // #2
alert(Inst_Soldier1.branch);
alert(Inst_Soldier2.branch);
alert(Inst_Soldier1.title);
alert(Inst_Soldier2.title);
alert(Inst_Soldier1.nature); // #3
alert(Inst_Soldier2.nature); // #3

이미 생성된 인스턴스도 뒤에 추가된 prototype 프로퍼티의 영향을 받을까?

답은 '그렇다' 이다. 자바스크립트는 인터프리터 언어디다. 그렇기 때문에 코드를 읽어들이는 순간 순간 마다 처리가 이루어진다. 위에서 살펴본 코드 처럼 이미 객체가 생기고 난 다음이더라도 다음 라인에서 코드를 읽어 들일때 새로운 prototype 프로퍼티가 추가되면 그 다음 라인에서 읽어들일 Inst_Soldier1와 Inst_Soldier2의 입장에서는 조건으로 주어진 프로퍼티를 자신의 객체에서 찾고 없을 경우 생성자 함수의 prototype을 뒤지게 된다.

    • 1) 서브클래스의 인스턴스(instance level)에서 프로퍼티를 찾는다.
    • 2) 그곳에 없었으면 다시 서브클래스의 프로토타입(prototype level)에서 프로퍼티를 찾는다.
    • 3) 역시 못 찾으면 슈퍼클래스의 프로토타입(prototype level)에서 다시 프로퍼티를 찾는다.



_proto_ 프로퍼티

이미 앞에서 여러번에 걸쳐 강조하였지만 너무나도 중요하기에 따로 분리해서 다시 한번 설명하겠다.

_proto_의 정의는 다음과 같다.

    • 어떤 객체가 만들어지든(new XXX()) 인터프리터는 자동으로 _proto_ 프로퍼티를 할당해 주게 된다.
    • _proto_ 프로퍼티는 그 객체의 생성자 함수의 prototype 프로퍼티에 대한 레퍼런스이다.
    • 따라서 모든 클래스의 prototype 객체와 그 클래스에서 생성된 인스턴스는 _proto_ 프로퍼티와 값을 가지고 있다.
    • _proto_ 프로퍼티 값은 인스턴스 자신이나 자신을 생성했던 클래스 안에서 원하는 프로퍼티를 찾을 수 없을 때 해당 프로퍼티를 어디에서 찾아야하는지의 장소를 나타낸다.


var Cs = function(name){
this.name = name;
this.Nick = 'daejang'
}
Cs.prototype.Nature = 'Pure';
inst_Cs = new Cs ('나이스 가이');
alert(Cs);
alert(Cs.prototype.constructor);
alert(Cs == Cs.prototype.constructor); // #1
alert(inst_Cs.__proto__); // #2
alert(Cs.prototype == inst_Cs.__proto__); // #3

#1 : 클래스 Cs 와 클래스 Cs.prototype.constructor 가 같은 것임을 보여주고 있다.

#2 : _proto_ 프로퍼티에는 객체가 들어가 있다는 것을 나타내고 있다.

#3 : 클래스 Cs 의 prototype은 인스턴스 inst_Cs 의 _proto_ 프로퍼티의 레퍼런스와 같다는 것을 보여주고 있다.

var Cs = function(name){
this.name = name;
}
Cs.prototype.A_0 = function(){
alert(this.name);
}
inst_Cs = new Cs ('나이스 가이');
inst_Cs.A_0(); // #1

#1 : 인스턴스 inst_Cs 에서 상속된 A_() 메서드를 호출하면 인터프리터는 내부적으로 inst_Cs._proto_ 프로퍼티를 통해 Cs.prototype.A_() 메서드를 호출하게 된다.

var Cs = function(name){
this.name = name;
}
Cs.prototype.A_0 = function(){
alert(this.name);
}
inst_Cs = new Cs ('나이스 가이');
alert(inst_Cs.__proto__ == Cs.prototype); // #1

#1 : 인스턴스 inst_Cs 가 클래스 Cs 에서 상속된 인스턴스임을 보여주고 있다.

var Person1 = function(){
}
Person1.prototype.nature = 'Pure';
var Person2 = function(){
}
Person2.prototype.nature = 'Very Pure';
var Soldier = function(name, branch){
this.name = name;
this.branch = branch;
}
Soldier.prototype = new Person1();
Soldier.prototype.title = 'Captain';
var Nurse = function(gender, ward){
this.gender = gender;
this.ward = ward;
}
Nurse.prototype = new Person2();
Nurse.prototype.title = 'Sir';
Inst_Soldier1 = new Soldier('나이스가이', 'Army');
alert(Inst_Soldier1.branch);
alert(Inst_Soldier1.name);
alert(Inst_Soldier1.title);
alert(Inst_Soldier1.nature);
Inst_Soldier1.__proto__ = Nurse.prototype; // #1
alert(Inst_Soldier1.branch); // #2
alert(Inst_Soldier1.name); // #2
alert(Inst_Soldier1.gender); // #3
alert(Inst_Soldier1.ward); // #3
alert(Inst_Soldier1.title); // #4
alert(Inst_Soldier1.nature); // #5

#1 : 인스턴스 생성 후 Nurse 클래스의 prototype 객체의 프로퍼티를 이용하도록 상속 링크를 걸어준 것이다.

#2 : 인스턴스 차원의 프로퍼티는 바뀐 것이 아니므로 여전히 Soldier 객체의 인스턴스 차원의 프로퍼티와 값을 사용하고 있다.

#3 : 일단 인스턴스 생성 후에 상속 링크가 변경되었으므로 생성 당시 전달된 인자에 의한 프로퍼티는 변경되지 않는다. gender 와 ward 는 Nurse 클래스의 인스턴스 차원의 프로퍼티이므로 Soldier 인스턴스에서는 해당 프로퍼티를 찾을 수 없다고 undefined 를 나타낸다.

#4 : 상속 링크를 변경시킨 결과로 Nurse 클래스의 prototype 객체의 프로퍼티 값을 보여준다.

#5 : Nurse 클래스의 슈퍼클래스의 프로토타입 객체를 그대로 물려 받으므로 차이가 없다.

위의 코드에서 볼 때, Inst_Soldir1 인스턴스는 prototype 객체에서 상속된 것이므로, Inst_Soldier1._proto_ 는 Soldier.prototype 객체가 된다. 또한 Soldier 클래스는 Person 클래스에서 상속된 것이므로, soldier.prototype._proto_ 는 Person.prototype 이 된다. 결국 서브클래스의 prototype 은 슈퍼클래스의 prototype 객체에서 물려 받게 되는 것이다.

지금까지 우리는 _proto_ 프로퍼티의 값을 설정해 주지 않았다. _proto_ 프로퍼티는 인스턴스나 클래스가 만들어 질 때 자동으로 값이 설정된 것이다. 물론 필요에 따라 이 _proto_  프로퍼티의 값을 바꿀 수도 있다. 아래처럼 _proto_ 프로퍼티의 값을 변경하게 되면 Inst_Soldier1._proto_ = Nurse.prototype; 처음 인스턴스를 생성했던 Soldier 클래스에서 상속 고리가 바뀌게 되는 것이다. 즉, 인터프리터 내부에서는 Nurse 클래스의 인스턴스로 간주하게 된다. 이렇게 되면 Soldier 클래스의 prototype 객체를 통해 상속된 프로퍼티들은 제거되게 된다.

var Person = function(name, id){ // #1
}
Person.prototype.nature = 'Pure';
Person.prototype.A_1 = A_0;
function A_0(){
alert('My name is ' + this.name);
alert('My ID is ' + this.id);
alert('My nature is ' + this.nature);
}
var Soldier = function(name, id){
this.name = name;
this.id = id;
}
Soldier.prototype .__proto__ = Person.prototype; // #2
var Nurse = function(name, id){
this.name = name;
this.id = id;
}
Nurse.prototype.__proto__ = Person.prototype; // #2
Inst_Soldier = new Soldier('Yong Min Lee', 'daejang');
Inst_Nurse = new Nurse('Min Ha Lee', 'minhalee');
Inst_Nurse.nature = 'Very Pure';
Inst_Soldier.A_1(); // #3
Inst_Nurse.A_1(); // #3

#1 : Person 클래스는 슈퍼클래스가 될 것이다.

Person = function(name, id){ 을

Person = function(){ 로 바꾸어도 차이가 없다.

#2 : Soldier 클래스와 Nurse 클래스 모두 동일하게 Person 클래스에서 상속된 것을 나타내고 있다. 주의해서 보아야 할 것은 이전의 예제코드들에서 보았던 상속 방법과 다른 방법을 사용하고 있다는 것을 알 수 있다. 여기서 _proto_ 는 인스턴스에게 어디서 prototype 객체를 찾아야 할지 가르쳐 주는 역할을 한다. 이 문장을 이해하기 쉽게 풀자면, 'Soldier 클래스(혹은 Nurse 클래스)의 프로퍼티는 오른쪽의 Person 클래스의 프로퍼티에서 상속되었다' 가 된다. 물론 이렇게 상속된 상태에서도 Soldier 클래스(혹은 Nurse 클래스)에 또 다시 프로퍼티를 추가할 수 있게 된다.

ex) Soldier.prototype.추가될프로퍼티명 = 값;

그리고 만일 이 Soldier 클래스(혹은 Nurse 클래스)를 또 다른 클래스에 상속시키게 되면 역시 새로 추가된 것과 슈퍼클래스에서 물려받은 프로퍼티 모두를 상속시켜줄 수 있게 된다.

#3 : Inst_Soldier 나 Inst_Nurse 에서 A_1 메서드를 호출하게 되면 다음과 같은 과정을 거치게 된다.

    • 먼저 해당 인스턴스에 A_1 메서드를 찾아보게 된다.
    • 당연히 해당 인스턴스에 없으므로 자신을 만든 클래스에서 다시 찾아보게 된다.
    • 물론 그곳에도 없으니 사다리타기를 계속해서 슈퍼클래스 즉, Person에서 prototype 객체의 프로퍼티인 A_1 을 찾게되면 다시 이에 링크된 A_0 을 찾아 호출하게 된다.
    • 이렇게 사다리타기를 해서 함수를 찾아 호출하게 되면 다시 함수는 해당 인스턴스에서부터 this.name, this.id, this.nature 를 찾게 된다.



참고 : http://wiki.gurubee.net/pages/viewpage.action?pageId=1507954



반응형