이 문서의 이용시 주의사항(Attention for this document)

이 페이지는 Prototype 공식 홈페이지의 "Defining classes and inheritance" 문서를 한국어로 번역한 문서입니다. 번역 오류정보 및 기타 제안사항은 제안 및 문의사항 페이지를 통해 보내주시기 바랍니다. 이 문서는 원저작자의 공유 조건인 Creative Commons 저작자표시-동일조건변경허락 3.0 Unported에 따라 이용하실 수 있습니다.

본문(Content)

Prototype에서 클래스 생성의 기반은 여전히 Class.create() 메소드이다. 프레임워크의 새로운 버전에서도 사용자의 클래스 기반 코드는 전처럼 동작할 것이다. 단지 이제는 직접적으로 객체의 prototype을 다룰 필요가 없어졌으며 속성을 복사하기 위해 Object.extend()를 사용할 필요도 없다.

예제(Example)

Prototype에서 클래스를 생성하고 상속하는 새로운 방법과 이전 방법을 비교해 보자.

/** 진부한 문법 **/

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();
// Person 클래스로부터 상속
Pirate.prototype = Object.extend(new Person(), {
	// say 메소드를 재정의
	say: function(message) {
		return this.name + ': ' + message + ', yarr!';
	}
});

var john = new Pirate('Long John');
john.say('ahoy matey');
// -> "Long John: ahoy matey, yarr!"

클래스의 prototype과 직접적으로 연동되는 부분과 Object.extend()를 사용하는 어색한 상속 테크닉에 주목하라. Personsay() 메소드를 재정의하는 Pirate에서는 클래스 기반 상속을 지원하는 다른 프로그래밍 언어와는 다르게, 오버라이딩된(overriden) 메소드를 호출할 방법이 없다.

이 문제는 개선되었다. 다음과 위 예제를 비교해보라:

/** 새롭게 도입된 문법 **/

// 속성은 직접적으로 `create` 메소드로 전달된다
var Person = Class.create({
	initialize: function(name) {
		this.name = name;
	},
	say: function(message) {
		return this.name + ': ' + message;
	}
});

// 서브클래스를 생성할 때 상속할 클래스를 지정한다
var Pirate = Class.create(Person, {
	// say 메소드를 재정의
	say: function($super, message) {
		return $super(message) + ', yarr!';
	}
});

var john = new Pirate('Long John');
john.say('ahoy matey');
// -> "Long John: ahoy matey, yarr!"

더이상 직접적으로 prototype을 해킹할 필요가 없어졌기 때문에 클래스와 서브클래스가 모두 더 짧아진 것을 확인할 수 있다. 또한 위 예제에는 "수퍼콜(supercall)"이라 불리는 새로운 기능의 데모도 포함되어 있다. (Ruby에서 사용하는 특별한 키워드인 super를 사용하여) Pirate#say가 오버라이딩한(overriden) 메소드를 호출하는 것이다.

모듈과 함께 사용하는 법(How to mix-in modules)

지금까지는 Class.create의 일반적인 사용법을 보았다.

var Pirate = Class.create(Person, { /* 인스턴스 메소드 */ });

그러나 사실상 Class.create가 받는 인수의 갯수는 자의적으로 변한다. (첫번째 인수가 다른 클래스인 경우) 새로 생성되는 클래스는 첫번째 인수를 상속한다. 이외의 모든 경우는 인스턴스 메소드(instance method)로서 추가되는데, 내부적으로는 addMethods에 의해 호출된다(다음 예제를 보라). 이 방식은 모듈을 통합하기에 편리하다.

// 모듈 정의
var Vulnerable = {
	wound: function(hp) {
		this.health -= hp;
		if (this.health < 0) this.kill();
	},
	kill: function() {
		this.dead = true;
	}
};

// 첫번째 인수는 클래스 객체가 아니다. 따라서 상속도 일어나지 않는다...
// 단순한 메소드로서 모든 인자를 복사한다
var Person = Class.create(Vulnerable, {
	initialize: function() {
		this.health = 100;
		this.dead = false;
	}
});

var bruce = new Person;
bruce.wound(55);
bruce.health; //-> 45

메소드 정의에서 $super 인수(The $super argument in method definitions)

서브클래스에서 메소드를 오버라이딩하되(override) 원본 메소드 역시 계속 해서 호출할 수 있기를 원한다면, 메소드에 레퍼런스를 부여할 필요가 있다. 메소드 정의에서 인수의 처음에 임시 인수 $super를 부여하면 레퍼런스를 지정할 수 있다. Prototype은 이를 인식하여 이 인수를 통해 사용자가 필요로 하는 메소드를 덧씌우게 된다(overriden). 그러나 외부에서 볼 때 Pirate#say 메소드는 여전히 하나의 인수를 갖는 것으로 취급되는데, 사용자는 이러한 세부 구현에 신경쓰지 않고 이전처럼 코드를 작성하면 된다.

프로그래밍 언어에서 상속의 타입(Types of inheritance in programming languages)

일반적으로 클래스 기반 상속과 프로토타입 상속으로 구별되는데, 자바스크립트는 후자의 케이스이다. 곧 나올 자바스크립트 2.0은 진정한 의미의 클래스 정의를 지원할 예정이지만, 최신 브라우저에 구현되어 있는 자바스크립트 언어의 현재 버전은 프로토타입 상속에 제한되어 있다.

물론, 프로토타입 상속은 매우 유용한 기능이기는 하지만 실제로 객체를 생성하기에는 다소 장황한 면이 있기도 하다. 이 점이 내부적으로 프로토타입 상속을 통해 (Ruby와 비슷한) 클래스 기반 상속을 흉내내는 이유이다. 이것은 암시하는 바가 있다. 예를 들어, PHP에서는 인스턴스 변수에 초기값을 정의할 수 있다.

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의 또 다른 인스턴스를 생성하려 한다면 어쩔 것인가?

var logger2 = new Logger;
logger2.log; // -> ['foo', 'bar']

// ... 어이, log는 비어있어야 하는 거 아냐?

볼 수 있는 것처럼, 새로운 인스턴스에서 빈 배열이 예상되지만 이전에 생성된 logger의 값이 존재한다. 사실상 모든 logger 객체는 값이 아니라 레퍼런스에 의해 복제되었기 때문에 하나의 배열 객체를 공유하게 된다. 올바른 방법은 인스턴스마다 디폴트 값으로 초기화하는 것이다.

var Logger = Class.create({
	initialize: function() {
		// 이것이 제대로된 방식이다
		this.log = [];
	},
	write: function(message) {
		this.log.push(message);
	}
});

클래스 메소드 정의(Defining class methods)

Prototype 1.6.0에서는 클래스 메소드에 대한 특별한 지원은 없다. 단순히 존재하는 클래스에 정의하기만 하면 된다.

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.extend(Pirate, {
	song: function(pirates) { ... },
	sail: function(crew) { ... },
	booze: ['grog', 'rum']
});

Pirate를 상속할 때 클래스 메소드는 상속되지 않는다. 이 기능은 Prototype의 이후 버전에서 추가될 예정이다. 그때까지는 수동으로 메소드를 복사하되 다음처럼 하지는 말도록:

var Captain = Class.create(Pirate, {});
// 이건 틀림!
Object.extend(Captain, Pirate);

클래스 생성자는 함수 객체인데 이는 단순히 어떤 클래스에서 다른 클래스로 모든 것을 복사하려 할 경우 문제의 여지가 있다. 운이 좋다면 Captainsuperclasssubclasses 속성을 오버라이드할 수 있을지도 모르지만, 이는 좋은 방법은 아니다.

특수 클래스 속성(Special class properties)

Prototype 1.6.0은 두 가지 특별한 클래스 속성인 subclasses, superclass를 정의하고 있다. 역할은 이름이 묘사하는 것과 같아서 각각 현재 클래스의 수퍼클래스와 서브클래스의 레퍼런스이다.

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#addMethods로 메소드를 추가하기(Adding methods on-the-fly with Class#addMethods)

Class#addMethods는 Prototype 1.6 RC0에서는 Class.extend로 불렸다. 이에 따라 코드를 적절히 업데이트 하라.

임시 메소드를 추가하고 싶은 클래스가 이미 정의되어 있다고 가정하자. 당연히 해당 메소드를 서브클래스와 메모리에 존재하는 모든 인스턴스에서 바로 사용하길 원하고 있다. 이것은 prototype 체인(chain)에 속성을 주입함으로써 이뤄지게 되는데, Prototype에서 이를 구현하는 가장 안전한 방법은 Class#addMethods를 사용하는 것이다.

var john = new Pirate('Long John');
john.sleep();
// -> ERROR: sleep은 메소드가 아니다

// pirate 뿐만 아니라, 모든 person은 sleep할 수 있어야 한다!
Person.addMethods({
	sleep: function() {
		return this.say('ZzZ');
	}
});

john.sleep();
// -> "Long John: ZzZ, yarr!"

sleep 메소드는 새롭게 생성될 Person 인스턴스에서 뿐만 아니라 Person의 서브클래스 및 현재 메모리에 존재하는 인스턴스에서도 바로 사용할 수 있다.

트랙백 목록(Trackback List)

이 글에 대한 감상/의견을 트랙백으로 보내주세요.

URL
이 글에는 트랙백을 보낼 수 없습니다