2007년 11월 19일 월요일

Prototype 1.6.0의 addMethods()에대한 고찰

요즘에 회사에서 JavaScript OOP 주제로 스터디를 하고 있는데요. 최근에 정식버전이 나온 prototype 1.6.0의 상속기능에 대한 주제로 K과장님(실명을 밝혀도 되려나? ^^;;;)께서 발표를 하셨는데 그 때 잘이해가 안되던 Class.addMethods()에 대해서 다시 정리해보게 되었습니다.
사용자 삽입 이미지

※ 본 포스트에 사용된 Animal클래스는 http://prototypejs.org/api/class/addMethods에 나와있는 클래스입니다.

1. firebug를 실행시켜보면 addMethods의 파라미터(source)는 클래스가 넘어오고 properties는 클래스가 가지고 있는 프로퍼티인 클래스 메소드, Function의 메소드와 prototype 등의 배열이 됩니다.

Object.keys(Animal) 을 출력했을 때 다음의 결과를 얻었습니다.

addMethods, superclass, subclasses, prototype, argumentNames, bind,
 bindAsEventListener, curry, delay, wrap, methodize, defer

2.
ancestor: ancestor는 this.superclass && this.superclass.prototype이 되게 됩니다. 이 때 && 연산의 결과를 알아보기 위해서 http://prototypejs.org/api/class/addMethods의 Animal 클래스로 테스트를 해봤습니다.

Animal을 출력했을 때는

function klass() {this.initialize.apply(this, arguments);}

Animal.prototype을 출력했을 때는
({initialize:(function (name, sound) {this.name = name;this.sound = sound;}),
 speak:(function () {alert(this.name + " says: " + this.sound + "!");})})

의 값을 얻었습니다.

그리고, Animal && Animal.prototype의 결과로는 Animal.prototype의 결과 값을 얻었습니다.

({initialize:(function (name, sound) {this.name = name;this.sound = sound;}),
 speak:(function () {alert(this.name + " says: " + this.sound + "!");})})


(궁금증, 개인적으로는 연산 결과로 logical 연산 값인 true나 false가 나올 줄 알았습니다. 왜그런지 아시는 분 있으신가요?)


3. for문과 if문의 부분은 $super가 첫 번째 argument로 정의되어 있는 Function을 찾는 경우를 의미합니다. (부모 클래스로부터 자식이 function을 상속받아서 $super로 부모의 메소드를 호출할 수 있는 경우가 되겠죠)

4. 이제 가장 궁금했던 value 부분을 살펴볼 것인데요. 가장 바깥의 Object.extend를 호출하는 부분은 value에 toString과 valueOf 메소드를 추가해주는 코드입니다. Object.extend의 첫 번째 파라미터로 넘겨지는 부분만 따로 때어내면 다음과 같습니다.
            (
                function(m) {
                      return function() {
                          return ancestor[m].apply(this, arguments)
                      };
                }
            )(property).wrap(method),

(이 코드가 어떻게 호출되면 과연 부모의 코드를 호출하게 되는 것일까 많은 고민을 했었습니다. -_-;;)

5. 먼저, 가장 처음 실행되는 부분만 따로 때서 생각해보면 (function(m){...})(property) 부분은 인터프리터에서 코드가 읽히면서 실행이 되게 되는데요. 인자로 받은 property의 값은 function(m)의 파라미터인 m으로 넘어가게 됩니다.
Animal.prototype.speak = function () { alert(this.name + " says: " + this.sound + "!"); }
이 코드가 for 루프로 들어가게 되면 m의 값이 'speak'로 넘어오게 되고 ancestor[m]은 function () { alert(this.name + " says: " + this.sound + "!"); }이 되게 됩니다. 즉, 실행된 코드는 실행시점에서 받은 파라미터를 넘겨서 Animal의  speak를 호출하게됩니다. (이 값이 나중에 Animal을 상속받는 자식의 메소드로 저장됩니다.)

            (
               function(m) {
                     return function() {
                         return ancestor[m].apply(this, arguments)
                     };
               }
           )(property)

여기까지 실행된 값은
function () { return ancestor[m].apply(this, arguments); } 됩니다.

6. 이제 마지막으로 wrap메소드를 살펴보면 다음과 같은 메소드로 구성됩니다. 위의 코드의 wrap()이 호출될 때 인자로 넘겨지는 wrapper는 Animal 클래스의 speak 메소드(function () { alert(this.name + " says: " + this.sound + "!"); })가 됩니다.

    wrap: function(wrapper) {
        // 그리고, __method는 5에서 넘겨지는
        // function() {return ancestor[m].apply(this, arguments);}
        // 가 되게 됩니다.
        var __method = this;
        return function() {
            // 그리고, 이 부모의 메소드가 첫 번째 인자로 넘어가게 됩니다. $super의 파라미터 위치에
            // 부모 메소드가 넘겨지게 됩니다.
            return wrapper.apply(this, [__method.bind(this)].concat($A(arguments)));
        }
    },

7. 그리고, 이 결과 값이 자식의 prototype[property]로 추가되게 됩니다.


P.S. 좀 정신사납게 적은 글입니다만, 참고용으로만 봐주세요. 전체 코드를 다 본게 아니라서.. 쿨헉~
(''  ;;; 언제 다 본 적 있냐고 물으시면 난감합니다.~~)

테스트 출력에 사용한 샘플 파일입니다. (허접이긴 하지만 참고용으로 올립니다. )

※  3에서 던진 질문의 답을 알아냈습니다. JavaScript에서는 Short-Circuit Evalation 연산을 사용하게 되는데요. (expression1) && (expression2)의 연산에서 앞의 expression1이 false면 뒤의 expression2의 연산을 실행하지 않는 것을 Short-Circuit Evaluation이라 합니다. (시험에도 나왔었던 기억이.. -_-;;;)

Animal && Animal.prototype의 경우에는 Animal이 reference 값이 있으므로 true의 값이 떨어지게 되는데요. 이 때 연산의 결과는 expression1의 값과 상관없이 expression2의 연산의 결과에만 의존하게 되므로 expression2의 연산을 boolean으로 값으로 evaluation하지 않습니다.
사용자 삽입 이미지
※ Wikipedia의 내용을 더 살펴보니 Short-Circuit Evaluation을 실행하는 경우 앞의 expression1이 true인 경우 뒤의 연산을 하지 않는, 즉 마지막 값을 그대로 넘겨주는 언어가 여럿 있군요 ^^

댓글 6개:

  1. 3번에 대한 질문은 간단하게 적자면...

    var a= null;

    var b= 4;

    alert( a && b);



    이것과



    var a= null;

    var b= 4;

    alert( a || b);



    이것의 차이라고 생각하시면 쉽겠네요.. 당연히 알고 계시는 부분이라 추측 ^-^



    var element = document.getElementById('target') || document.body;



    element 가 취하는 값은 or 연산자와 and 연산자에 따라 취하는 값이 틀립니다.

    .. 당연히 모두~~~~~ 알고 계시는 부분이라 추측 ^-^



    위의 addMethod 에서

    var ancestor = this.superclass && this.superclass.prototype;

    이 코드의 의미를 알아보자면..

    this.superclass 가 존재하지 않는다면 this.superclass.prototype 도 존재하지 않는 것과 같은 의미를 두기 위함이구요. 만약 존재한다면 ancetor 는 this.superclass.prototype을 reference하게 됩니다.



    또한 prototype.js 자체가 prototype 기반으로 개발되고 지향하고 있어 overriding, inheritance, addMethod 할때 Aninimal.prototype.method 로 메서드를 추가하기 위함인거 같기도 합니다.



    만약

    var ancestor = this.superclass;

    라고만 했다면 addMethod의 실제 메서드가 추가되는 방식은

    Animal.method 가 되겠죠.



    설명은 어설프지만.. 도움이 되셨으면... ^^;;

    답글삭제
  2. Statement 상에서의 && 나 || 의 사용은 Perl 같은 언어에서는 많이 사용하는 방식입니다.

    본래, if (condition() == true) { do_something() } 같은 것도, condition() && do_something() 처럼 쓰는 것이죠.

    결과적으로는 && 를 기준으로 "앞의 결과가 참이면 뒤도 실행"과 같은 거죠.

    마찬가지 방식으로 || 의 경우, 앞의 결과가 거짓(false, 0, undefined, null)이면 뒤까지 실행하죠.

    그래서, Cross-Browsing 코드로 이벤트 핸들러를 짤 때, function handler(e) { var ev = e || event; var key = ev.which || ev.keyCode; } 와 같이 짜게 되는 것입니다.

    도움이 되셨길 ^^

    답글삭제
  3. @Rhio.kim - 2007/11/20 17:02
    답변 감사합니다. ^^

    덕분에 많은 도움이 되었습니다. Rhio.Kim님의 블로그도 자주 방문할께요~~

    답글삭제
  4. @K과장 - 2007/11/20 18:35
    앗.. 어느새 다셨군요 ^^

    도움 감사합니다~~~ ^^

    답글삭제
  5. 저도 어설프게 알고 접근했던 부분인데 K과장님의 설명과 Short-Circuit Evaluation 를 보니 확실히 알았습니다.

    저도 옷 사러 많이 올께요.. ^-^*

    답글삭제
  6. @Rhio.kim - 2007/11/20 22:26
    넵~ 자주 놀로오세요 기대할께요 ^^

    답글삭제