객체지향 프로그래밍(5): subClass
subClass는 다음 세가지를 활용한다.
1. 함수의 프로토타입 체인
2. extend 함수
3. 인스턴스를 생성할 때 생성자 호출(_init)
subClass는 상속받을 클래스에 넣을 변수 및 메소드가 담긴 객체를 인자로 받아 부모 함수를 상속받는 자식 클래스를 만든다. 여기서 부모 함수는 subClass() 함수를 호출할 때 this 객체를 의미한다.
예를들면...
var SuperClass = subClass(obj);
var SubClass = SuperClass.subClass(obj);
이처럼 SuperClass를 상속받는 subClass를 만들고자 할 때, SuperClass.subClass() 의 형식으로 호출하게 구현한다. 참고로 최상위 클래스인 SuperClass는 자바스크립트의 Function을 상속받게 한다.
함수 subClass의 구조는...
function subClass(obj) {
1. 자식 클래스 (함수 객체) 생성
2. 생성자 호출
3. 프로토타입 체인을 활용한 상속 구현
4. obj를 통해 들어온 변수 및 메소드를 자식 클래스에 추가
5. 자식 함수 객체 반환
}
function subClass(obj) {
/*... */
var parent = this;
var F = function() {};
var child = function() {};
F.prototype = parent.prototype;
child.prototype = new F();
child.prototype.constructor = child;
child.parent = parent.prototype;
child.parent_constructor = parent;
/**... */
return child;
}
자식 클래스는 child라는 이름의 함수 객체를 생성함으로써 만들어졌다. 부모 클래스를 가리키는 parent는 this를 그대로 참조한다.
child.parent_constructor에 부모의 생성자를 참조시켰다.
사용자는 인자로 넣은 객체를 자식 클래스에 넣어 자식 클래스를 확장할 수 있다.
for(var i in obj) {
if(obj.hasOwnProperty(i)) {
child.prototype[i] = obj[i];
}
}
클래스의 인스턴스가 생성될 때, 클래스 내에 정의된 생성자가 호출돼야 한다. 물론 부모 클래스의 생성자 역시 호출되어야 한다. 이를 자식클래스 안에 구현한다.
var child = function() {
var parent = child.parent;
if(parent._init) {
parent._init.apply(this, arguments);
}
if(child.prototype._init) {
child.prototype._init.apply(this, arguments);
}
};
이 코드는 겉보기엔 큰 문제가 없어 보이지만..
parent._init이나 child.prototype._init을 찾을 때, _init 프로퍼티가 없으면 프로토타입 체인으로 상위 클래스의 _init 함수를 찾아서 호출할 수 있다. 따라서 다음과 같이 hasOwnProperty 함수를 활용하자.
var child = function() {
var parent = child.parent;
if(parent.hasOwnProperty('_init')) {
parent._init.apply(this, arguments);
}
if(child.prototype.hasOwnProperty('_init')) {
child.prototype._init.apply(this, arguments);
}
};
위 코드는 단순히 부모와 자식이 한 쌍을 이루었을 때만 제대로 작동한다. 자식을 또 다른 함수가 다시 상속 받았을 땐?
var SuperClass = subClass();
var SubClass = SuperClass.subClass();
var Sub_SubClass = SubClass.subClass();
var instance = new Sub_SubClass();
instance를 생성할 때, 그 상위 클래스의 상위 클래스인 SuperClass의 생성자가 호출되지 않는다. 따라서 부모 클래스의 생성자를 호출하는 코드는 재귀적으로 구현할 필요가 있다. 이미 child.parent_constructor에 부모의 생성자 함수를 참조시켜 놓았으므로 구현에 문제가 없다.
var child = function() {
var _parent = child.parent_constructor;
if(_parent && _parent !== Function) {
// 현재 클래스의 부모 생성자가 있으면 그 함수를 호출한다.
// 다만 부모가 Function인 경우는 최상위 클래스에 도달했으므로 실행하지 않는다.
_parent.apply(this, arguments); // 부모 함수의 재귀적 호출
}
if(child.prototype.hasOwnProperty('_init')) {
child.prototype._init.apply(this, arguments);
}
};
subClass 보완
parent를 단순히 this.prototype으로 지정해서는 안 된다. 우리는 처음에 최상위 클래스를 Function을 상속받는 것을 정했는데, 현재 코드에는 이를 처리하는 코드가 없다.
따라서
parent = this;
를
if(this === window) {
var parent = Function;
}else {
var parent = this;
}
이를 좀더 깔끔하게...
var parent = this === window ? Function : this; // Node.js의 경우 global을..ㅋ
또 하나 빠진 부분이 있다. subClass 안에서 생성하는 자식 클래스의 역할을 하는 함수는 subClass 함수가 있어야 한다.
child.subClass = arguments.callee;
arguments.callee는 현재 호출된 함수를 의미하는데, 현재 호출된 함수가 subClass이므로 child.subClass는 subClass 함수를 참조한다.
전체 subClass 함수 코드
function subClass(obj) {
var parent = this === window ? Function : this;
var F = function() {};
var child = function() {
var _parent = child.parent;
if(_parent && _parent !== Function) {
_parent.apply(this, arguments);
}
if(child.prototype._init) {
child.prototype._init.apply(this, arguments);
}
};
F.prototype = parent.prototype;
child.prototype = new F();
child.prototype.constructor = child;
child.parent = parent;
child.subClass = arguments.callee;
for(var i in obj) {
if(obj.hasOwnProperty(i)) {
child.prototype[i] = obj[i];
}
}
return child;
}
subClass 함수로 상속 예제.
function subClass(obj) {
var parent = this === window ? Function : this;
var F = function() {};
var child = function() {
var _parent = child.parent;
if(_parent && _parent !== Function) {
_parent.apply(this, arguments);
}
if(child.prototype._init) {
child.prototype._init.apply(this, arguments);
}
};
F.prototype = parent.prototype;
child.prototype = new F();
child.prototype.constructor = child;
child.parent = parent;
child.subClass = arguments.callee;
for(var i in obj) {
if(obj.hasOwnProperty(i)) {
child.prototype[i] = obj[i];
}
}
return child;
}
var person_obj = {
_init: function() {
console.log('person init');
},
getName: function() {
return this._name;
},
setName: function(name) {
this._name = name;
}
};
var student_obj = {
_init: function() {
console.log('student init');
},
getName: function() {
return 'Student name: ' + this._name;
}
};
var Person = subClass(person_obj); // Person 클래스 정의
var person = new Person(); // person init 출력
person.setName('devjones');
console.log(person.getName()); // devjones
var Student = Person.subClass(student_obj); // Student 클래스 정의
var student = new Student(); // person init, student init 출력
student.setName('grindman');
console.log(student.getName()); // Student name: grindman
console.log(Person.toString()); // Person이 Function을 상속받는지 확인
유의사항
생성자 함수가 호출되는가?
부모의 메소드가 자식 인스턴스에서 호출되는가?
자식 클래스가 확장 가능한가?
최상위 클래스인 Person은 Function을 상속받는가?
subClass 함수에 클로저 적용
var F = function() {};
이 함수 객체는 subClass 함수가 호출될 때마다 생성된다. 클로저로 단 한 번만 생성되게 수정하자.
var subClass = function() {
var F = function() {};
var subClass = function(obj) {
/*... */
}
return subClass;
}();
즉시 실행함수로 새로운 컨텍스트를 만들어서 F() 함수 객체를 생성하였다. 그리고 이 F() 함수 객체를 참조하는 안쪽의 subClass() 함수를 반환받는다. 이렇게 하면 F() 함수 객체는 클로저에 엮여서 가비지 컬렉션의 대상이 되지 않고, subClass() 함수를 호출할때마다 사용된다.
subClass 함수와 모듈 패턴을 이용한 객체지향 프로그래밍
var person = function(arg) {
var name = undefined;
return {
_init: function(arg) {
name = arg ? arg : 'devjones';
},
getName: function() {
return name;
},
setName: function(arg) {
name = arg;
}
};
}
Person = subClass(person());
var devjones = new Person('dev-jones');
console.log(devjones.getName());
Student = Person.subClass();
var student = new Student('student');
console.log(student.getName());
person 함수 객체는 name의 정보를 캡슐화시킨 객체를 반환받는 역할을 한다. 이렇게 반환받은 객체는 subClass() 함수의 인자로 들엉가 클래스의 역할을 하는 Person 함수 객체를 완성시킬 수 있다. 이제 Person 함수 객체를 활용하여 상속을 구현할 수 있다.