6. 자바스크립트 객체지향 프로그래밍 #4 객체지향 프로그래밍 응용 예제

2021. 11. 11. 17:14JavaScript/basic

이전글

https://yonghwankim-dev.tistory.com/167

 

6. 자바스크립트 객체지향 프로그래밍 #3 캡슐화

이전글 https://yonghwankim-dev.tistory.com/166 6. 자바스크립트 객체지향 프로그래밍 #2 상속 이전글 https://yonghwankim-dev.tistory.com/165 6. 자바스크립트 객체지향 프로그래밍 #1 클래스, 생성자, 메서..

yonghwankim-dev.tistory.com

 

본 글은 INSIDE JAVASCRIPT 도서의 내용을 복습하기 위해 작성된 글입니다.

 

1. 클래스의 기능을 가진 subClass 함수

프로토타입을 이용한 상속과 클래스 기반의 상속에서 소개한 내용을 바탕으로 기존 클래스와 같은 기능을 하는 자바스크립트 함수를 생성하는 것을 살펴봅니다. 이 함수에서는 앞서 소개한 다음 세 가지를 활용해서 구현합니다. 함수의 이름은 subClass로 합니다.

  • 함수의 프로토타입 체인
  • extend 함수
  • 인스턴스를 생성할 때 생성자 호출(여기서는 생성자를 _init 함수로 정의)

1.1 subClass 함수 구조

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) 자식 함수 객체 반환 */
}

 

1.2 자식 클래스 생성 및 상속

function subClass(obj){

	...
	
    let parent = this;
    let F = function(){};
    
    /*(1) 자식 클래스(함수 객체) 생성 */
    let child = function(){

    };

    /*(3) 프로토타입 체인을 활용한 상속 구현 */
    F.prototype = parent.prototype;
    /*(2) 생성자 호출 */
    child.prototype = new F();
    child.prototype.constructor = child;
    child.parent = parent.prototype;
    child.parent_constructor = parent;
    
    ...

    /*(5) 자식 함수 객체 반환 */
    return child;
}

자식 클래스는 child라는 이름의 함수 객체를 생성함으로써 만들어졌습니다. 부모 클래스를 가리키는 parent는 this를 그대로 가리킵니다. 위 예제의 관계를 그림으로 표현하면 아래와 같습니다.

 

1.3 자식 클래스 확장

이제 사용자가 인자로 넣은 객체를 자식 클래스에 넣어 자식 클래스를 확장할 차례입니다.

for(var i in prop){
    if(obj.hasOwnProperty(i)){
    	child.property[i] = obj[i];
    }
}

위 코드와 같이 extend() 함수의 역할을 하는 코드를 넣습니다. 여기서는 간단히 얕은 복사로 객체의 프로퍼티를 복사하는 방식을 택하였습니다.

 

1.4 생성자 호출

클래스의 인스턴스가 생성될 때, 클래스 내에 정의된 생성자가 호출되어야 합니다. 물론 부모 클래스의 생성자 역시 호출되어야 합니다. 이를 자식 클래스 안에서 구현합니다.

let child = function(){
        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(){
	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의 생성자가 호출이 되지 않습니다. 따라서 부모 클래스의 생성자를 호출하는 코드는 재귀적으로 구현해야 합니다. 아래의 코드는 개선한 코드입니다.

let child = function(){
            let _parent = child.parent_constructor;
            
            // 현재 클래스의 부모 생성자가 있으면 그 함수를 호출합니다.
            // 다만 부모가 Function인 경우는 최상위 클래스에 도달했으므로
            // 실행하지 않습니다.
            if(_parent && _parent !== Function){
                _parent.apply(this, arguments);  // 부모 함수의 재귀적 호출
            }

            if(child.prototype.hasOwnProperty("_init")){
                child.prototype._init.apply(this,arguments);
            }
};

 

1.5 subClass 보완

parent를 단순히 this.prototype으로 지정해서는 안됩니다. 최상위 클래스는 Function이어야 합니다. 위의 코드에서는 이를 처리하는 코드가 없습니다.

let parent = this===window ? Function : this;

subClass 안에서 생성하는 자식 클래스의 역할을 하는 함수는 subClass 함수가 있어야 합니다.

child.subClass = arguments.callee;

arguments.callee는 현재 호출된 함수를 의미하는데, 현재 호출된 함수가 subClass이므로 child.subClass는 subClass 함수를 참조합니다.

 

1.6 subClass 활용 및 전체 코드

function subClass(obj){

    let parent = this===window ? Function : this;
    let F = function(){};
    
    /*(1) 자식 클래스(함수 객체) 생성 */
    let child = function(){
        let _parent = child.parent;
        
        // 현재 클래스의 부모 생성자가 있으면 그 함수를 호출합니다.
        // 다만 부모가 Function인 경우는 최상위 클래스에 도달했으므로
        // 실행하지 않습니다.
        if(_parent && _parent !== Function){
            _parent.apply(this, arguments);  // 부모 함수의 재귀적 호출
        }

        if(child.prototype._init){
            child.prototype._init.apply(this,arguments);
        }
    };

    /*(3) 프로토타입 체인을 활용한 상속 구현 */
    F.prototype = parent.prototype;
    /*(2) 생성자 호출 */
    child.prototype = new F();
    child.prototype.constructor = child;
    child.parent = parent;
    child.subClass = arguments.callee;

    /*(4) obj를 통해 들어온 변수 및 메서드를 자식 클래스에 추가 */
    for(let i in obj){
        // 프로토타입 체인을 올라가지 않고 해당 객체 내에서만 찾음
        if(obj.hasOwnProperty(i)){
            child.prototype[i] = obj[i];
        }
    }

    /*(5) 자식 함수 객체 반환 */
    return child;
}

let person_obj = {
    _init : function(){
        console.log("person init");
    },
    getName : function(){
        return this._name;
    },
    setName : function(name){
        this._name = name;
    }
};

let student_obj = {
    _init : function(){
        console.log("student init");
    },
    getName : function(){
        return "Student Name: " + this._name;
    }
};

let Person = subClass(person_obj);  // Person 클래스 정의
let person = new Person();          // Expected Output : person init 출력
person.setName("zzoon");
console.log(person.getName());  // Expected Output : zzoon

let Student = Person.subClass(student_obj); // Student 클래스 정의
let student = new Student();    // Expected Output : person init, student init 출력
student.setName("iamhjoo");
console.log(student.getName()); // Expected Output : Student Name : iamhjoo

console.log(Person.toString());
  • 생성자 함수가 호출되는가?
  • 부모의 메서드가 자식 인스턴스에서 호출되는가?
  • 자식 클래스가 확장 가능한가?
  • 최상위 클래스인 Person은 Function을 상속받는가?

1.7 subClass 함수에 클로저 적용

let subClass = function(){
    let F = function(){};

    let subClass = function(obj){
        ...
    }
    return subClass;
}();

...

즉시 실행 함수로 새로운 컨텍스트를 만들어서 F() 함수 객체를 생성하였습니다. 그리고 이 F() 함수 객체를 참조하는 안쪽의 subClass() 함수를 반환받습니다. 이렇게 하면 F() 함수 객체는 클로저에 엮어서 가비지 컬렉션의 대상이 되지 않고, subClass() 함수를 호출할 때마다 사용이 가능합니다.

 

위 예제의 전체 소스코드는 다음과 같습니다.

let subClass = function(){
    let F = function(){};

    let subClass = function(obj){
        let parent = this===window ? Function : this;
        
        let child = function(){
            let _parent = child.parent;
            
            // 현재 클래스의 부모 생성자가 있으면 그 함수를 호출합니다.
            // 다만 부모가 Function인 경우는 최상위 클래스에 도달했으므로
            // 실행하지 않습니다.
            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(let i in obj){
            // 프로토타입 체인을 올라가지 않고 해당 객체 내에서만 찾음
            if(obj.hasOwnProperty(i)){
                child.prototype[i] = obj[i];
            }
        }
        return child;
    }
    return subClass;
}();

let person_obj = {
    _init : function(){
        console.log("person init");
    },
    getName : function(){
        return this._name;
    },
    setName : function(name){
        this._name = name;
    }
};

let student_obj = {
    _init : function(){
        console.log("student init");
    },
    getName : function(){
        return "Student Name: " + this._name;
    }
};

let Person = subClass(person_obj);  // Person 클래스 정의
let person = new Person();          // Expected Output : person init 출력
person.setName("zzoon");
console.log(person.getName());  // Expected Output : zzoon

let Student = Person.subClass(student_obj); // Student 클래스 정의
let student = new Student();    // Expected Output : person init, student init 출력
student.setName("iamhjoo");
console.log(student.getName()); // Expected Output : Student Name : iamhjoo

console.log(Person.toString());

 

2. subClass 함수와 모듈 패턴을 이용한 객체지향 프로그래밍

이 절에서는 모듈 패턴으로 캡슐화를 구현하여, subClass() 함수로 상속을 구현하는 방법을 살펴봅니다.

/* 캡슐화 */
let subClass = function(){
    let F = function(){};

    let subClass = function(obj){
        let parent = this===window ? Function : this;
        
        let child = function(){
            let _parent = child.parent;
            
            // 현재 클래스의 부모 생성자가 있으면 그 함수를 호출합니다.
            // 다만 부모가 Function인 경우는 최상위 클래스에 도달했으므로
            // 실행하지 않습니다.
            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; // arguments.callee : subClass(obj){ ... }

        for(let i in obj){
            // 프로토타입 체인을 올라가지 않고 해당 객체 내에서만 찾음
            if(obj.hasOwnProperty(i)){
                child.prototype[i] = obj[i];
            }
        }
        return child;
    }
    return subClass;
}();



const person = function(arg){
    let name = undefined;

    return {
        _init : function(arg){
            name = arg ? arg : "zzoon";
        },
        getName : function(){
            return name;
        },
        setName : function(arg){
            name = arg;
        }
    };
    
};

Person = subClass(person());
let iamhjoo = new Person("iamhjoo");
console.log(iamhjoo.getName()); // Expected Output : iamhjoo

Student = Person.subClass();
let student = new Student("student");
console.log(student.getName()); // Expected Output : student

person 함수 객체는 name의 정보를 캡슐화시킨 객체를 반환받는 역할을 합니다. 이렇게 반환받은 객체는 subClass() 함수의 인자로 들어가 클래스의 역할을 하는 Person 함수 객체를 완성시킵니다. 이제 Person 함수 객체를 활용하여 상속을 구현할 수 있습니다.

 

References

source code : https://github.com/yonghwankim-dev/javascript\_study
송형주, 고현준 저, 『인사이드 자바스크립트』, 한빛미디어(2014)