코드/JS

프로토타입 심화

Yeah-Panda 2013. 11. 26. 13:50

원본글 링크: http://cafe.naver.com/hacosa?1310101133000


개요

 

자바스크립트는 이전 강좌에서 말한 것처럼 확장을 전혀 고려하지 않는 함수 기반 형태로 주로 사용되고 있다. 하지만 웹 기술은 나날이 발전 하고 있고 쇼핑몰의 장바구니, 블로그 관리 페이지에서 볼 수 있는 통계 UI 등과 같은 인터랙티브 요소를 제공하는 사이트들이 점점 많아지고 있다. 그렇다면 자바스크립트와 그 확장에 있어서 중요한 것은 무엇일까?


그건 바로 프로토타입이다. Ajax과 최근에 대세(?)가 되고 있는 HTML5에 대한 관심이 방대한 자바스크립트 라이브러리와 대규모 웹 기반 어플리케이션 개발을 증폭시켰고, 이로 인해 확장성의 중요성이 한층 높아졌다.




 

 

프로토타입

  

자바스크립트에서 함수(또는 클래스)는 'prototype'라는 특수한 프로퍼티를 가지고 있다. 이를 사용해서 String과 Number 같은 내장 클래스는 물론이고 사용자 정의 클래스를 포함한 모든 객체를 확장할 수 있다. 즉, 클래스 상속이 아닌 프로토타입을 이용해서 클래스의 메소드와 프로퍼티를 추가할 수 있다. 이는 상속 개념과는 약간 다르다. 상속은 자식 클래스가 원하든 원치 않든 부모의 모든 속성을 물려 받는 것이고, 프로토타입을 이용한 객체 확장은 기존의 클래스에 기능을 외부에서 추가하여 사용하는 것이다.


아래 예제에 나오는 trim 메소드는 문자열에서 앞과 뒤에 있는 공백 문자들을 제거한다. (Link)

1
2
3
5
6
7
9
10
String.prototype.trim = function() {
     return (this.replace(/^[\s\xA0]+/, "").replace(/[\s\xA0]+$/, ""));
};

var sObj = new String(       Prototype Test         ");
sTxt = sObj.trim();

alert("--" + sTxt + "--");


위의 소스 코드의 출력 메시지는 "--Prototype Test--"이 다. 기존의 String 내장 객체에 prototype를 사용하여 좌우 공백을 없애는 trim 메소드를 구현하였다. 프로토타입을 사용하여 구현하면 해당 페이지는 물론이고, 이 라이브러리를 로드하는 모든 페이지에서도 사용할 수 있다. 즉, 앞서 말한 것처럼 새로운 클래스를 생성한 것이 아니라 기존의 클래스의 기능을 확장한 것이다. 바로 이것이 클래스 기반과 프로토타입의 차이점이다.


아래 예제와 같이 클래스가 아닌 인스턴스에 trim 메소드를 추가하는 방법도 있다. (Link)

1
2
3
4
6
7
8
var sObj = new String("         Prototype Test       ");
sObj.trim = function() {
     return (this.replace(/^[\s\xA0]+/, "").replace(/[\s\xA0]+$/, ""));
};

sTxt = sObj.trim();
alert("--" + sTxt + "--");

그러나 이렇게 하면 해당 인스턴스만 메소드에 접근할 수 있기 때문에 prototype를 사용해서 String 클래스 자체에 추가해주는 것이 더 좋을 것이다. 그럼, 마지막으로 jQuery에서 자주 사용되고 있는 each 메소드를 prototype를 사용하여 구현해보자.


아래 예제는 자바스크립트 내장 클래스인 Array에 추가될 each 메소드이다. (Link)

1
2
3
4
5
7
8
9
10
11
Array.prototype.each = function(callback) {
     for(i = 0; i < this.length; i++) {
          callback(this[i]);
     }
};

var test = [ { name : 'a' }, { name : 'b' }, { name : 'c' } ];
test.each(function(elem) {
     alert(elem.name);
});


위의 소스 코드는 "a""b""c" 세 번의 메시지를 출력한다. each 메소드는 콜백 함수의 파라메터를 통해 test 배열에 포함되어 있는 객체들을 차례대로 호출한다. (콜백 함수에 대해서는 다음 장에서 자세히 다루도록 하겠다.)


하 지만 뭔가 이상하다. jQuery를 사용해본 개발자라면 알겠지만 해당 배열에 포함되어 있는 객체들은 콜백 함수의 파라메터로 넘겨주지 않는다. 콜백 함수의 파라메터로는 해당 객체의 배열 index 값을 보내며 각각의 객체들은 콜백 함수 내에서 this 키워드로 접근한다.


아래 예제는 test 배열 변수의 값을 얻어와 index 값과 해당 객체의 name값을 출력한다. (jQuery $.each 메소드 예제)

1
2
3

4

5

var test = [ { name : 'a' }, { name : 'b' }, { name : 'c' } ];


$.each(test, function(index) {
     alert(index + " = " + this.name);
});


jQuery 를 사용하다보면 each 메소드처럼 콜백 함수 내에서 this로 배열의 요소들을 접근하는 경우가 많다. 하지만 대부분의 사용자들은 왜 저렇게 구현되는지 관심을 가지지 않는다. 필자는 콜백 함수 내에서 this로 접근할 수 있는 방법을 찾기 위해 오랫동안 고민했었다. 이런 고민의 해결의 중심에는 바로 프로토타입이 있었다. (Link)

1
2
3
4
5
6
8
9
10
11
12
Array.prototype.each = function(callback) {
     for(i = 0; i < this.length; i++) {
          callback.prototype = this[i];
          new callback(i);
     }
};

var test = [ { name : 'a' }, { name : 'b' }, { name : 'c' } ];
test.each(function(index) {
     alert(index + " = " + this.name);
});


위의 예제에서 정말 중요한 부분이 있다. 바로 3~4라인이 다. 첫 번째 예제에서는 바로 callback 함수의 파라메터로 this[i] 값을 넘겼다. 하지만 여기서는 callback 함수의 prototype를 this[i]로 정의하였다. 이전 강좌에서 자바스크립트의 함수는 클래스와 같다라고 설명한적이 있다. 


즉, 함수는 클래스이기 때문에 자신의 원형을 prototype 프로퍼티를 통해 정의할 수 있다. 이로써 callback 함수는 name 프로퍼티를 가진 객체의 원형 클래스가 된다. 이는 콜백 함수 내에서 this 키워드를 통해 name 프로퍼티에 접근할 수 있음을 의미한다






프로토타입 체인

  

앞서 설명했듯이 함수(또는 클래스)는 'prototype'이라는 특수한 프로퍼티가 존재한다고 하였다. 만약에 함수 이름이 'Test'라고 가정을 하고, 'new' 키워드를 사용하여 인스턴스를 생성하였을 때, 'Test.prototype'은 방금 생성된 인스턴스의 추상적인 존재인 원형(=프로토타입)이 된다.


기본적으로 인스턴스를 생성하지 않았을 때의 프로토타입은 자바스크립트 최상위 Object 클래스를 참조한다.

1
2
3
4
5
6
7
var Test = function() {
     this.print = function() {
          alert("this.print");
     };
};

console.log(Test.prototype);







생성된 인스턴스는 자신의 추상적인 존재인 Test 클래스의 맴버들을 참조하게 된다. 그리고 Test 클래스의 맴버는 Test 클래스의 프로토타입을 참조하게 되고, Test 클래스의 프로토타입은 최상위 클래스인 Object 클래스의 프로토타입을 참조한다. 이런 식으로 인스턴스 생성 시점부터 최상위 클래스까지 순차적으로 연결되는 것을 바로 프로토타입 체인이라고 한다.


참고로 생성된 인스턴스는 자신의 클래스(또는 함수) 안에서의 'this'와 동일하다고 생각하면 된다. 아래 예제를 보면 인스턴스를 생성하지 않고 단순히 호출하였을 때의 값과 인스턴스를 생성했을 때의 값이 다르다는 것을 알 수 있다.

1
2
4
5
6
7
8
9
10
11
12
13
14
15
var Test = function() {
     console.log(this);

     this.print = function() {
          alert("this.print");
     };
};

// 함수 호출
Test();

// 인스턴스 생성
var ttt = new Test();
console.log(ttt);





'Test();'와 같이 단순히 함수를 호출하였을 때의 this는 Window 클래스를 참조한다. 하지만 인스턴스가 생성되었을 때의 this는 Window 클래스가 아닌 Test 클래스를 참조한다. 그리고 IE를 제외하고 크롬이나 파폭에서는 생성된 인스턴스에는 '__proto__'라는 프로퍼티가 존재하는데, 이것은 바로 앞서 말했던 'Test.prototype'과 동일하다고 생각하면 된다.


그럼, 지금부터 본격적으로 프로토타입 체인과 관련된 예제를 보도록 하자. (Link)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

var Test = function() {
     this.print= function() {
          alert("this.print");
     };
};

Object.prototype.print= function() {
     alert("Object.prototype.print");
};

Test.prototype.print= function() {
     alert("Test.prototype.print");
};

new Test().print();


위의 예제의 출력 값은 'this.test'이다. 인스턴스가 생성될 때 첫번째로 자신의 클래스의 맴버를 참조한다. 만약에 호출하는 맴버 메소드가 없을 경우에는 어떤 값이 출력이 될까? 두번째 예제를 보도록 하자.

1
2
3
4
5
6
7
8
9
10
11
var Test = function() { };

Object.prototype.print= function() {
     alert("Object.prototype.print");
};

Test.prototype.print= function() {
     alert("Test.prototype.print");
};

new Test().print();

위의 예제의 출력 값은 'Test.prototype.test'이다. 최종적으로 프로토타입 체인은 Object 클래스의 프로토타입을 참조하게 된다.

그럼, 세번째 예제를 보도록 하자.

1
2
3
4
5
6
7
var Test = function() { };

Object.prototype.print= function() {
     alert("Object.prototype.print");
};

new Test().print();

모두 예상했듯이 출력 값은 'Object.prototype.test'이다. 이 메소드마저 정의되어 있지 않다면 비로소 메소드가 존재하지 않는다라는 에러 메시지가 발생한다.





이렇게 프로토타입 체인을 사용한다면 클래스들의 관계를 유기적으로 연결할 수 있다. 어떻게 보면 프로토타입 체인은 OOP에서의 상속과도 비슷한 개념이라 볼 수 있다.


자바스크립트에서는 인스턴스를 생성하였을 때에만 프로토타입 체인이 발생하는 것이 아니다. 이전 강좌인 'JS Tip & Tech (1) - OOP 심화'에서도 설명했었던 정적 메소드 호출시에도 프로토타입 체인은 발생한다.


그럼, 아래 예제를 보도록 하자. (Link)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var Test = function() {
     this.print= function() {
          alert("this.print");
     };
};

Object.prototype.print= function() {
     alert("Object.prototype.print");
};

Function.prototype.print = function() {
     alert("Function.prototype.print");
};

Test.prototype.print= function() {
     alert("Test.prototype.print");
};

Test.print = function() {
     alert("Test.print");
}

Test.print();

19~21라인을 보면 Test 클래스의 정적 메소드를 정의하였다. 앞서 설명했듯이 'new' 키워드를 사용하여 생성된 인스턴스의 추상적인 존재가 바로 Test 클래스라고 하였다. 하지만 인스턴스와 별도의 스코프에 존재하는 정적 메소드의 원형은 Function 클래스이다.


그렇기 때문에 위의 예제에서 Test 클래스의 맴버로 선언된 메소드(2~4라인)와 'Test.prototype.print' 메소드(15~17라인)는 정적 메소드와 관련이 없는 별개의 메소드라고 생각하면 된다.





앞에 두 순서도를 보면 알겠지만 자바스크립트에서 모든 프로퍼티와 메소드의 프로토타입 체인의 마지막은 Object 클래스를 참조한다.






constructor 프로퍼티


프로토타입은 'constructor'라는 특수한 프로퍼티를 가지고 있는데, 이것은 바로 자신의 원형이 되는 클래스를 참조한다. (Link)

1
2
3
4
5
6
7

8

9

10

11

12

13

14

15

16

17

18

 var Test = function() {
     this.print= function() {
          this.constructor.show();
     };
};

Test.show = function() {
     alert("Test.show");
};

var t1 = new Test();
var t2 = new Test();
console.log(t1.constructor === t2.constructor);

var ct1 = new t1.constructor();
var ct2 = new t2.constructor();
ct1.print();
ct2.print();


먼저 13라인을 보자. 두 인스턴스의 'constructor'는 바로 Test 클래스를 참조하기 때문에 'true'가 출력이 된다.  또한 'constructor'는 Test 클래스 그 자체이기 때문에 15~16라인과 같이 새로운 인스턴스를 생성할 수도 있다. 그리고 인스턴스는 클래스 내부에서 'this'와 동일하기 때문에 'constructor' 프로퍼티를 가지고 있다. 그렇기 때문에 정적 메소드인 'Test.show'를 호출할 수 있는 것이다.


참고로 클래스 내부에서 'new this.constructor();'와 같이 인스턴스를 생성하면 무한 루프에 빠지게 되니 주의해야 한다.

1
2
3
4
5
6
7

8

9

10

11

var Test = function() {
     this.print= function() {
          alert("this.print");
     };

     var tt = new this.constructor();
     tt.print();
};

var ttt = new Test();
ttt.print();


'코드 > JS' 카테고리의 다른 글

컨텍스트, 스코프  (0) 2013.12.05
&&연산자  (0) 2013.11.28
프로토타입체인  (0) 2013.11.26
제이쿼리 요소 존재 확인, 자식요소존재 확인  (0) 2013.11.22
숫자 3단위마다 콤마 삽임  (0) 2013.11.21