http://prototypejs.org/learn/class-inheritance
이전 버젼의 Prototoype.js 에서는, 클래스 생성을 위해서 Class.create() 메소드를 제공했다.
지금까지 이렇게 생성된 클래스에는, constructor 인 initialize() 메소드가 호출되는 것이 유일한 기능이었다.
Prototype.js 1.6 에서는 - 물론 직전 버전부터 수차례의 단계가 더 있긴 하다 - inheritance 지원이 추가되어,
사용자는 보다 풍부한(?) 클래스를 보다 쉽게 생성할 수 있게 되었다.
Prototype.js 에서의 클래스 생성은 여전히 Class.create() 인 것은 변하지 않는다(변할 수 없다).
또한 이전과 동일하게 class-based 코드를 작성하고 사용할 수 있다.
변한 것은,
property 들을 가져오기 위해서 object 의 prototype 을 건드리거나 Object.extend()를 사용할 필요가 없어졌다는 것이다.
예를 들면,
클래스를 생성하고 상속을 구현하기 위해 이전 버젼과 새로운 버젼으로 작성된 두 코드를 비교해보자.
protytpe 에 직접 접근해야 하고, Object.extend() 를 사용해서 거추장스럽게 상속을 구현하고 있다.
또한, say() 메소드를 재정의한 Pirate 에서는 overriden 메소드를 호출할 방법이 없다.
다음 새로운 코드를 비교해보자.
prototype 에 접근하지 않고 class 정의와 동시에 subclassing 하는 것이 얼마나 간편해 졌는지 모른다.
또한 "supercall" -루비에서는 super 키워드로 이미 구현된- 또는 overridden 메소드 호출이 가능해졌다.
이를 이용해서 모듈 mix-in 하는 법
다음은 전형적인 Class.create() 호출 방법이다.
사실은, Class.create() 메소드가 한정되지 않은 매개변수를 갖는 것이다(역자 주; 메소드 정의에서는 매개변수가 2개이지만 내부적으로 볼때 그 수가 한정되지 않았다는 뜻). 첫번째 매개변수는 -그 클래스가 또다른 클래스라면- 상속받으려는 클래스이고, 그 외의 매개변수는 instance method 의 형태로 추가된다 -내부적으로는 addMethods()라는 메소드의 호출을 수반한다- . 이것을 이용하면 모듈의 mix-in 을 용이하게 할 수 있다.
메소드 정의에서 제공되는 $super 키워드
subclass 에서 메소드를 override 하고자 할 때, 그러나 여전히 기존의 메소드의 호출도 가능하게 하고 싶을 때라면, reference 가 필요하다. 이 reference 는, 해당 메소드 앞에 $super 키워드를 사용하면 얻을 수 있다. 그러나 외부에는 Pirate#say 메소드는 단하나의 매개변수만 필요한 메소드로 표출된다. 이러한 세부적인 구현 사항이 개발자의 소스코드의 작동에 영향을 미치지는 않는다.
각종 프로그래밍 언어에서의 상속 구현의 유형들
일반적으로 우리는, 클래스 기반 상속과 프로토타입 상속 -자바스크립트의 유형- 을 구분짓는다. Javascript 2.0 에서 클래스가 제대로 지원되기 전까지, Javascript 는 프로토타입 상속에 제한될 수 밖에 없는 실정이다.
프로토타입 상속, 물론, 굉장히 유용한 특성이긴 하지만, 객체를 다루기에는 좀 번거로운 것이 사실이다. 이것이 우리가 내부적으로 프로토타입 상속을 사용해서 클래스 기반 상속을 흉내내려는 이유이다-루비에서 처럼-. 이것은 확실히 의미가 있는데, instance 에 대해서 보자면, PHP에서는 instance variable 에 초기값을 지정할 수 있다. (역자 주; 이 부분을 제대로 이해하기 위해서는 class-based programming 에 있어서의 instance variable과 class method 에 대한 개념이 필요하다. Javascript : The Definitive Guide 9장 참고.)
Prototype 에서도 동일하게 이것이 가능하다.
이것은 제대로 작동한다. 그러나 만약 새로운 Logger instance 를 만들면?
var logger2 = new Logger;
logger2.log; // -> ['foo', 'bar']
//... hey, the log should have been empty!
목격한 바대로, 새로운 instance 의 빈 배열에 대한 우리의 기대와 상관없이, 이전 logger의 배열과 동일한 배열을 갖게 된다. 실제적으로, value 에 의한 것이 아닌 reference 에 의한 복제가 일어나기 때문에, 모든 logger object 는 동일한 배열 object 를 공유하게 된다.instance 에 초기값을 지정하는 올바른 방법은 다음과 같다.
class method 정의
Prototype 1.6.0 에서 class method 에 대한 특별한 지원은 없다. 기존 클래스에 대해 간단한 이의 구현은 다음과 같다.
만약 한꺼번에 여러개를 정의할 필요가 있다면, 예전과 같이 Object.extend 를 사용하면 된다.
현재로서는, Pirate 로부터 상속을 받아도 class method 는 상속되지 않는다. 이는 다음 버전에서 추가될 예정이다. 그때까지는 수동으로 복제하는 수밖에 없는데, 다만, 다음과 같이 하지는 말자.
클래스 생성자는 Function object 이다. 그리고 만약, 클래스 내의 모든 것을 또다른 클래스로 몽땅 복제한다면, 혼란만 가중될 뿐이다. 썩 좋지는 않지만, 최선의 경우는, subclass들과 그 속성들을 overriding 하는 것이다.
특별한 클래스 속성들
Prototype 1.6.0 에서는 2개의 특별한 클래스 속성 -superclass, subclasses- 이 정의된다. 말그대로, 현재 클래스의 superclass 와 subclass 를 가리킨다.
Person.superclass
// -> null
Person.subclasses.length
// -> 1
Person.subclasses.first() == Pirate
// -> true
Pirate.superclass == Person
// -> true
Captain.superclass == Pirate
// -> true
Captain.superclass == Person
// -> false
보다 쉬운 class introspection 을 위한 속성들이다.
Class#addMethods()
Class#addMethods 는 Prototype 1.6 RC0 에서 Class.extend 였다. 이에 따라 코드를 수정할 것을 공지한다.
추가적으로 메소드를 더하려고 하는 기정의된 클래스가 있다고 가정하자. 자연적으로, 이 추가적인 메소드들이 subclass 및 이미 메모리상에 존재하는 instance 에도 사용가능하길 원한다. 이것은 prototype chain 에 속성을 끼워넣는 것으로 가능한데, Prototype을 사용하는 경우 가장 안전한 방법이 바로 Class#addMethods 를 사용하는 것이다.
이 sleep 메소드는 새로운 Person instance 들 뿐 아니라, subclass 와 이미 메모리상에 존재하는 instance 들에도 호출가능하다.
끝.
Defining classes and inheritance
이전 버젼의 Prototoype.js 에서는, 클래스 생성을 위해서 Class.create() 메소드를 제공했다.
지금까지 이렇게 생성된 클래스에는, constructor 인 initialize() 메소드가 호출되는 것이 유일한 기능이었다.
Prototype.js 1.6 에서는 - 물론 직전 버전부터 수차례의 단계가 더 있긴 하다 - inheritance 지원이 추가되어,
사용자는 보다 풍부한(?) 클래스를 보다 쉽게 생성할 수 있게 되었다.
Prototype.js 에서의 클래스 생성은 여전히 Class.create() 인 것은 변하지 않는다(변할 수 없다).
또한 이전과 동일하게 class-based 코드를 작성하고 사용할 수 있다.
변한 것은,
property 들을 가져오기 위해서 object 의 prototype 을 건드리거나 Object.extend()를 사용할 필요가 없어졌다는 것이다.
예를 들면,
클래스를 생성하고 상속을 구현하기 위해 이전 버젼과 새로운 버젼으로 작성된 두 코드를 비교해보자.
/** obsolete syntax **/
var Person = Class.create();
Person.prototype = {
initialize : function(name) {
this.name = name;
},
say : function(message){
return this.name + ' : ' + message;
},
};
var guy = new Person('Miro');
guy.say('hi');
// -> "Miro : hi"
var Pirate = Class.create();
// inherite from Person class
Pirate.prototype = Object.extend(new Person(), {
// redefine the speak method
say : function(message) {
return this.name + ' : ' + message + ', yarr!';
},
});
var john = new Pirate('Long John');
john.say('ahoy matey');
// -> "Long John : ahoy matey, yarr!"
protytpe 에 직접 접근해야 하고, Object.extend() 를 사용해서 거추장스럽게 상속을 구현하고 있다.
또한, say() 메소드를 재정의한 Pirate 에서는 overriden 메소드를 호출할 방법이 없다.
다음 새로운 코드를 비교해보자.
/** new, preferred syntax **/
// properties are directly passed to 'create' method
var Person = Class.create({
initialize : function(name) {
this.name = name;
},
say : function(message) {
return this.name + ' : ' + message;
},
});
// when subclassing, specify the class you want to inherit from
var Pirate = Class.create(Person, {
// redefine the speak method
say : function($super, message){
return $super(message) + ', yarr!';
},
});
var john = new Pirate('Long John');
john.say('ahoy matey');
// -> "Long John : ahoy matey, yarr!"
prototype 에 접근하지 않고 class 정의와 동시에 subclassing 하는 것이 얼마나 간편해 졌는지 모른다.
또한 "supercall" -루비에서는 super 키워드로 이미 구현된- 또는 overridden 메소드 호출이 가능해졌다.
이를 이용해서 모듈 mix-in 하는 법
다음은 전형적인 Class.create() 호출 방법이다.
var Pirate = Class.create(Person, { /* instance methods */ } );
사실은, Class.create() 메소드가 한정되지 않은 매개변수를 갖는 것이다(역자 주; 메소드 정의에서는 매개변수가 2개이지만 내부적으로 볼때 그 수가 한정되지 않았다는 뜻). 첫번째 매개변수는 -그 클래스가 또다른 클래스라면- 상속받으려는 클래스이고, 그 외의 매개변수는 instance method 의 형태로 추가된다 -내부적으로는 addMethods()라는 메소드의 호출을 수반한다- . 이것을 이용하면 모듈의 mix-in 을 용이하게 할 수 있다.
// define a module
var Vulnerable = {
wound: function(hp) {
this.health -= hp;
if (this.health < 0) tihs.kill();
},
kill : function() {
this.dead = true;
},
};
// the first argument isn't a class object, so there is no heritance ...
// simply mix in all the arguments as methods:
var Person = Class.create(Vulnerable, {
initialize : function() {
this.health = 100;
this.dead = false;
},
});
var bruce = new Person;
bruce.wound(55);
bruce.health; // -> 45
메소드 정의에서 제공되는 $super 키워드
subclass 에서 메소드를 override 하고자 할 때, 그러나 여전히 기존의 메소드의 호출도 가능하게 하고 싶을 때라면, reference 가 필요하다. 이 reference 는, 해당 메소드 앞에 $super 키워드를 사용하면 얻을 수 있다. 그러나 외부에는 Pirate#say 메소드는 단하나의 매개변수만 필요한 메소드로 표출된다. 이러한 세부적인 구현 사항이 개발자의 소스코드의 작동에 영향을 미치지는 않는다.
각종 프로그래밍 언어에서의 상속 구현의 유형들
일반적으로 우리는, 클래스 기반 상속과 프로토타입 상속 -자바스크립트의 유형- 을 구분짓는다. Javascript 2.0 에서 클래스가 제대로 지원되기 전까지, Javascript 는 프로토타입 상속에 제한될 수 밖에 없는 실정이다.
프로토타입 상속, 물론, 굉장히 유용한 특성이긴 하지만, 객체를 다루기에는 좀 번거로운 것이 사실이다. 이것이 우리가 내부적으로 프로토타입 상속을 사용해서 클래스 기반 상속을 흉내내려는 이유이다-루비에서 처럼-. 이것은 확실히 의미가 있는데, instance 에 대해서 보자면, PHP에서는 instance variable 에 초기값을 지정할 수 있다. (역자 주; 이 부분을 제대로 이해하기 위해서는 class-based programming 에 있어서의 instance variable과 class method 에 대한 개념이 필요하다. Javascript : The Definitive Guide 9장 참고.)
class Logger {
public $log = array();
function write($message) {
$this->log[] = $message;
}
}
$logger = new Logger;
$logger->write('foo');
$logger->write('bar');
$logger->log; // -> ['foo', 'bar']
Prototype 에서도 동일하게 이것이 가능하다.
var Logger = Class.create({
initialize : function() { },
log : [],
write : function(message) {
this.log.push(message);
},
});
var logger = new Logger;
logger.log; // -> []
logger.write('foo');
logger.write('bar');
logger.log; // -> ['foo', 'bar']
이것은 제대로 작동한다. 그러나 만약 새로운 Logger instance 를 만들면?
var logger2 = new Logger;
logger2.log; // -> ['foo', 'bar']
//... hey, the log should have been empty!
목격한 바대로, 새로운 instance 의 빈 배열에 대한 우리의 기대와 상관없이, 이전 logger의 배열과 동일한 배열을 갖게 된다. 실제적으로, value 에 의한 것이 아닌 reference 에 의한 복제가 일어나기 때문에, 모든 logger object 는 동일한 배열 object 를 공유하게 된다.instance 에 초기값을 지정하는 올바른 방법은 다음과 같다.
var Logger = Class.create({
initialize : function() {
// this is the right way to do it:
this.log = [];
},
write : function(message) {
this.log.push(message);
},
});
class method 정의
Prototype 1.6.0 에서 class method 에 대한 특별한 지원은 없다. 기존 클래스에 대해 간단한 이의 구현은 다음과 같다.
Pirate.allHandsOnDeck = function(n) {
var voices = [];
n.times(function(i) {
voices.push(new Pirate('Sea Dog').say(i+1));
});
return voices;
}
Pirate.allHandsOnDeck(3);
// -> ["Sea Dog: 1, yarr!", "Sea Dog: 2, yarr!", "Sea Dog: 3, yarr!"]
만약 한꺼번에 여러개를 정의할 필요가 있다면, 예전과 같이 Object.extend 를 사용하면 된다.
Object.extent(Pirate, {
song : function(pirates) {...},
sail : function(crew) {...},
booze : ['grog', 'rum'],
});
현재로서는, Pirate 로부터 상속을 받아도 class method 는 상속되지 않는다. 이는 다음 버전에서 추가될 예정이다. 그때까지는 수동으로 복제하는 수밖에 없는데, 다만, 다음과 같이 하지는 말자.
var Captain = Class.create(Pirate, {});
// this is wrong!
Object.extend(Captain, Pirate);
클래스 생성자는 Function object 이다. 그리고 만약, 클래스 내의 모든 것을 또다른 클래스로 몽땅 복제한다면, 혼란만 가중될 뿐이다. 썩 좋지는 않지만, 최선의 경우는, subclass들과 그 속성들을 overriding 하는 것이다.
특별한 클래스 속성들
Prototype 1.6.0 에서는 2개의 특별한 클래스 속성 -superclass, subclasses- 이 정의된다. 말그대로, 현재 클래스의 superclass 와 subclass 를 가리킨다.
Person.superclass
// -> null
Person.subclasses.length
// -> 1
Person.subclasses.first() == Pirate
// -> true
Pirate.superclass == Person
// -> true
Captain.superclass == Pirate
// -> true
Captain.superclass == Person
// -> false
보다 쉬운 class introspection 을 위한 속성들이다.
Class#addMethods()
Class#addMethods 는 Prototype 1.6 RC0 에서 Class.extend 였다. 이에 따라 코드를 수정할 것을 공지한다.
추가적으로 메소드를 더하려고 하는 기정의된 클래스가 있다고 가정하자. 자연적으로, 이 추가적인 메소드들이 subclass 및 이미 메모리상에 존재하는 instance 에도 사용가능하길 원한다. 이것은 prototype chain 에 속성을 끼워넣는 것으로 가능한데, Prototype을 사용하는 경우 가장 안전한 방법이 바로 Class#addMethods 를 사용하는 것이다.
var john = new Pirate('Long John');
john.sleep();
// -> ERROR: sleep is not a method
// every person should be able to sleep, not just pirates!
Person.addMethods({
sleep: function() {
return this.say('ZzZ');
},
});
john.sleep();
// -> "Long John: ZzZ, yarr!"
이 sleep 메소드는 새로운 Person instance 들 뿐 아니라, subclass 와 이미 메모리상에 존재하는 instance 들에도 호출가능하다.
끝.
최근 덧글