Study/JavaScript

[JavaScript][source analysis] 클래스 상속

Bluesky_ 2009. 4. 25. 18:07
반응형
자바스크립트에 메소드 오버로딩이 없는 것과 마찬가지로 클래스 상속 또한 없다.

하지만 구현은 가능하다.
JSON의 창시자로, JavaScript The Good parts의 저자로 유명한 Douglas Crockford가 구현한 소스이다.
원글 : http://javascript.crockford.com/inheritance.html

이 소스의 분석을 시작하기 전에 먼저 말해야 할 것이 있다.
이 코드를 제안한 Crockford 마저 버린 소스라는 것이다. -ㅅ-;;;

프로토타입적이고 함수적인 JavaScript의 소스에서 클래스 상속을 구현하긴 했지만 실제로 자신조차 사용하질 않는다고 말미에 달아놓은 소스이다.
하지만 JavaScript를 공부하기엔 좋은 예제이다.
이 소스를 이해하게 되면 prototype과 함수의 실행 관계에 대해 알 수 있게 된다.
Function.prototype.method = function (name, func) {
    this.prototype[name] = func;
    return this;
};
Function.method('inherits', function (parent) {
    var d = {}, p = (this.prototype = new parent());
    this.method('uber', function uber(name) {
        if (!(name in d)) {
            d[name] = 0;
        }        
        var f, r, t = d[name], v = parent.prototype;
        if (t) {
            while (t) {
                v = v.constructor.prototype;
                t -= 1;
            }
            f = v[name];
        } else {
            f = p[name];
            if (f == this[name]) {
                f = v[name];
            }
        }
        d[name] += 1;
        r = f.apply(this, Array.prototype.slice.apply(arguments, [1]));
        d[name] -= 1;
        return r;
    });
    return this;
});
Function.method('swiss', function (parent) {
    for (var i = 1; i < arguments.length; i += 1) {
        var name = arguments[i];
        this.prototype[name] = parent.prototype[name];
    }
    return this;
});

클래스 상속을 구현한 소스는 위와 같으며 이를 사용한 클래스는 아래와 같다.
function Parenizor(value) {    //이와 같은 생성자가 있다.
    this.setValue(value);
}

Parenizor.method('setValue', function (value) {    //생성자의 메소드들을 선언한다.
    this.value = value;
    return this;
});

Parenizor.method('getValue', function () {
    return this.value;
});

Parenizor.method('toString', function () {
    return '(' + this.getValue() + ')';
});
myParenizor = new Parenizor(0);    //이런 식으로 생성자를 호출하여 객체를 생성한다.
myString = myParenizor.toString();

function ZParenizor(value) {    //상속을 받을 생성자
    this.setValue(value);
}

ZParenizor.inherits(Parenizor);     //inherits를 통해 Parenizor를 ZParenizor로 상속시킨다.

ZParenizor.method('toString', function () {
    if (this.getValue()) {
        return this.uber('toString');
    }
    return "-0-";
});

myZParenizor = new ZParenizor(0);    //상속을 정의한 ZParenizor 생성자를 통해 객체를 생성
myString = myZParenizor.toString();

편의상 상속을 선언한 부분을 선언부분이라고 하고 이를 호출한 부분을 생성부분이라고 하자.
선언부분 코드 1라인에 놓인 함수는 일종의 헬퍼 함수이다.
함수를 호출하여 사용하는 것이 아닌 함수에 대해 상속을 구현해야 하기 때문에 함수를 정의할 수 있는 헬퍼 함수를 정의하였다.

헬퍼 함수를 만드는 데에는 prototype 속성이 사용되었다.
prototype에 대해서는 2009/04/14 - [Study_JavaScript] - [JavaScript][advanced] prototype property

위 글을 통해 소개한 적이 있으므로 참고하면 된다.

위의 소스의 실행에 대해 먼저 알아야 할 사항이 있다.
prototype의 선언
prototype의 실행
함수의 선언
함수의 실행

위 4가지 과정의 흐름이 어떻게 되는가 이다.
함수의 선언에 관해서는 기존에 설명한 글이 있다.
2009/04/04 - [Study_JavaScript] - [JavaScript][basic] 함수의 호출

함수를 선언하는 것과 실행하는 것과 마찬가지로
prototype은 함수를 선언하기 전에 함수를 선언하는 것이라고 생각하면 된다.
따라서 prototype의 선언->함수의 선언이 되고
함수를 호출-> 함수의 선언을 실행하는 것이 되는 것이다.

호출받은 name을 이름으로 하는 Function을 만들어서 그 기능을 func라는 익명함수로 재정의 한다.

5라인에 놓인 함수는 상속을 정의한 함수이다.
첫 선언 부분을 보면 "inherits"라는 이름의 메소드가 parent를 가지고 호출을 하게 되면 헬퍼함수에 의해 해당 이름을 가진 함수가 parent를 가지고 선언이 된다.

확실히 짚고 넘어가야 할 부분은
'Function이라는 객체(Function도 객체이다.) 또한 prototype을 가지고 있으며 이를 통해 메소드의 선언이 가능하다는 점이다.

위의 소스의 흐름을 살펴보면 우선 Function은 선언이 되고 실행이 되지 않으므로 선언 부분은 그대로 넘어가고 생성부분의 17라인에서 해당 생성자 함수가 호출이 된다.

18라인의 객체의 사용도 진행이 되고 상속을 받을 생성자를 선언한 다음
24라인에 오게 되면 생성자 객체에 대해 inherits 메소드가 프로토타입을 통해 실행이 된다.
이때 매개변수로 Perenizor 생성자 함수가 넘어가게 된다.

실행되는 내용은 선언 부분의 5라인에서 부터의 inherits 메소드이다.
순서대로 6라인에서 우선 d라는 이름의 JSON 객체와 p라는 이름의 JSON객체가 선언이 되었으며
매개변수로 넘어간 parent (Parenizor)가
  1. new parent()를 통해 객체가 생성이 되어
  2. this.prototype(ZParenizor의 prototype) 에 바인딩 되고
  3. 이 값이 p라는 내부 변수에 참조된다.
  4. d라는 이름의 빈 값의 내부 변수도 선언이 된다.
  5. 생성된 객체에 만약 uber메소드가 있으면 uber 메소드에 대한 특별한 처리를 한다.

여기까지가 상속에 대한 선언부분이다.

생성부분을 보면 크게 2가지로 나누어서 보면 된다.
Parenizor라는 객체에 대해 선언하여 사용한 부분(1~18라인) 과
ZParenizor라는 객체에 대해 선언하여 사용한 부분(20~34라인)이다.

이중 ZParenizor는 Parenizor라는 이름의 객체를 prototype 선언부분(첫번째 소스 부분)에서 정의한 inherits라는 메소드를 통해 상속을 하게 된다.

Parenizor는 어려울 것이 없다.
기존에 사용하던 객체를 선언하고, 해당 객체의 메소드를 선언하였다.
Function이 Function.prototype.method를 사용한 것과 같은 형태로 Parenenizor를 사용하였으며 이는 JSON의 문법이다.
함수도 객체이기 때문에 생성자 함수에 대해서 JSON의 형태로 메소드를 추가한 형태이다.
이 또한 자바스크립트 코드의 확장성이 좋은 기능이므로 취향에 따라 사용하면 될 것이다.

26라인을 보면 ZParenizor를 새로운 생성자 함수로 선언하고 이 생성자 함수에 Parenizor를 inherits 메소드를 통해 상속을 시켰다.
이는 위에 설명한 JSON 형태로 메소드를 추가한 것과 마찬가지로 inherits라는 메소드가 ZParenizor에 추가된 것이다.
이때 p라는 내부 변수에는 Parenizor라는 생성자 함수의 객체가 추가되게 되어진다. (이것이 상속의 핵심이다.)
재선언하지 않은 Parenizor에 존재하는 메소드를 호출하는 경우 this.prototype에 parent가 생성자로 바인딩 된 상태이기 때문에 Parenizor의 메소드가 호출되게 되어진다.

다음 단계로 7라인 이후의 uber 메소드가 헬퍼함수를 통해 선언되며 이를 호출하는 시점에 수행되는 메소드이다.
생성 부분의 26라인에서 재정의된 toString 메소드를 보면 this.getValue()가 true인 경우 수행되도록 되어 있다.

즉 17라인에서 매개변수를 0이 아닌 이외의 값으로 넘어가게 되면 uber 메소드가 실행이 되며
uber 메소드는 14라인에 설정되어 있는 동일 이름(toString)의 메소드가 실행되게 선언이 된다.

이와 같이 선언이 된 이후 실제 해당 함수에 대한 객체가 33라인에서 myZParenizor라는 이름으로 생성이 되면 선언된 함수들이 객체에 포함이 되게 되며 prototype으로 바인딩 된 함수는 객체에 포함이 되지 않은 상태로 그 기능을 수행하게 된다.

위 생성 부분에선 uber함수를 호출할 때 getValue()가 있으면 호출하도록 되어있어서 0값이 넘어간 경우 없다고 인식을 하여 uber method가 동작을 하지 않는다.

만약 0이 아닌 값을 가지고 생성을 하게 되는 경우 uber 메소드가 실행 되어진다.
처음 실행시의 uber 메소드의 실행 내용을 보면 다음과 같다.

this.method('uber', function uber(name) { 
	var f, r, t = d[name], v = parent.prototype;
	f = p[name];
	if (f == this[name]) {
		f = v[name];
	}
	d[name] += 1;
	r = f.apply(this, Array.prototype.slice.apply(arguments, [1]));
	d[name] -= 1;
	return r;
}

inherits 내부 변수 d와 p에 어떤 값을 담고 uber 메소드의 내부 변수 f, r, t, v엔 어떠한 값이 담겨지는지 보면
v에는 parent의 prototype이 바인딩 되고, f에는 p[name]을 통해 parent가 가지고 있는 메소드가 담겨지게 된다.
만약 f가 현재 메소드의 name함수와 동일하다면 f에는 현재 함수의 prototype이 연결된
반응형