[javascript] 클래스

5 minute read

자바스크립트는 프로토타입 기반 언어이기 때문에 상속이 존재하지 않으며, ES6에서는 클래스 문법이 추가되었으나, 일정 부분은 프로토타입을 활용하고 있기 때문에 ES5에서 클래스를 흉내내는 구현방식을 알아두는 것은 도움이 된다.

1. 자바스크립트의 클래스

var Rectangle = function(width, height){    // 생성자
    this.width = width;
    this.height = height;
};
Rectangle.prototype.getArea = function(){   // (prototype) 메서드
    return this.width * this.height;
};
Rectangle.isRectangle = function(instance){ // static 메서드
    return instance instanceof Rectangle && instance.width > 0 && instance.height > 0;
};

var rect1 = new Rectangle(3, 4);
console.log(rect1.getArea());   // 12
console.log(rect1.isRectangle(rect1));  // Error
console.log(Rectangle.isRectangle(rect1));  // true

rect1.getArea()의 경우, rect1.__proto__.getArea에 접근하여 결과를 호출한다. 이처럼 인스턴스에서 직접 호출할 수 있는 메서드를 프로토타입 메서드라 한다.

isRectangle()메서드의 경우는 스태틱 메서드 이므로, 인스턴스에서 접근할 수 없으며 생성자 함수를 this로 해야 호출할 수 있는 것이다.

이 차이를 유념해야할 것이다.

2. 클래스 상속

2.1. 기본구현

프로토타입 파트에서 보았던 예시를 다시 보자

var Grade = function(){
    var args = Array.prototype.slice.call(arguments);
    for(var i = 0; i < args.length; i++){
        this[i] = args[i];
    }
    this.length = args.length;
};
Grade.prototype = [];
var g = new Grade(100, 80);

자바스크립트에서 클래스 상속을 구현했다는 것은 프로토타입 체이닝을 잘 연결한 것으로 이해하면된다.

예제에서 문제는 length 프로퍼티가 configurable하다는 점과 Grade.prototype에 빈 배열을 참조시켰다는 점이다. 이 문제에 대해 살펴보자.

...
g.push(90);
console.log(g); // Grade { 0: 100, 1: 80, 2: 90, length: 3 }

delete g.length;
g.push(70);
cosole.log(g);  // Grade { 0: 70, 1: 80, 2: 90, length: 1 }

length 프로퍼티를 삭제하는 코드 때문에 push값이 0번 인덱스에 들어가고 length가 1이 되는 현상이 발생한다. 내장객체인 배열 인스턴스의 length는 configurable 속성이 false라 삭제가 불가능하지만, Grade는 일반 객체의 성질을 가지므로 삭제가 가능한 문제가 발생하는 것이다.

사용자가 정의한 두 클래스 사이에서 상속관계 구현하기

var Rectangle = function(width, height){
    this.width = width;
    this.height = height;
};
Rectangle.prototype.getArea = function(){
    return this.width * this.height;
};
var rect = new Rectangle(3, 4);
console.log(rect.getArea());    // 12

// var Square = function(width){
//     this.width = width;
//     this.height = width;
// };
// Square.prototype.getArea = fucntion(){
//     return this.width * this.height;
// };

var Square = function(width){
    Rectangle.call(this, width, width);
};
Square.prototype = new Rectangle();

var sq = new Square(5);
console.log(sq.getArea());  // 25

정사각형 Square 클래스와 유사한 사각형 Rectangle 클래스를 상위 클래스로 지정한다.

Square 생성자 함수 내부에서 Rectangle 생성자 함수를 함수로써 호출하고 메서드를 상속하기 위해 Square의 프로토타입 객체에 Rectangle의 인스턴스를 부여했다.

그러면 sq 인스턴스의 __proto__Rectangle 클래스를 가리키고 있을 것이다. (187p 그림 7-7 참고)

그러나 여전히 임의로 Square.prototype에 값을 할당할 경우 원치않는 결과가 나올 것이며, constructor가 여전히 Rectangle을 바라보고 있는 문제도 있다.

2.2 클래스가 구체적인 데이터를 지니지 않도록 하기

prototype이 구체적인 데이터를 지니지 않게 하는 가장 간단한 방법은 만들고 나서 프로퍼티들을 지운 후 추가할 수 없도록 하는 것이다.

delete Square.prototype.width;
delete Square.prototype.height;
Object.freeze(Square.prototype);

예제 1 - 인스턴스 생성 후 프로퍼티 제거

var extendClass1 = function(SuperClass, SubClass, subMethods){
    SubClass.prototype = new SuperClass();
    for(var prop in SubClass.prototype){
        if(SubClass.prototype.hasOwnProperty(prop)){
            delete SubClass.prototype[prop];
        }
    }
    if(subMethods){
        for(var method in subMethods){
            SubClass.prototype[method] = subMethods[method];
        }
    }
    Object.freez(SubClass.prototype);
    return SubClass;
};

var Square = extendClass1(Rectangle, function(width){
    Rectangle.call(this, width, width);
});

예제 2 - 빈 함수를 이용

더글라스 크락포트가 제시하여 대중적으로 알려진 방법.

아무런 프로퍼티를 생성하지 않는 빈 생성자 함수(Bridge)를 만들어 BridgeprototypeSuperClass.prototype을 바라보게 한 다음 SubClass.prototype에는 Bridge의 인스턴스를 할당하게 하는 방법.

...
var Bridge = function(){};
Bridge.prototype = Rectangle.prototype;
Square.prototype = new Bridge();
Object.freeze(Square.prototype);
var extendClass2 = (function(){
    var Bridge = function(){};
    return function(SuperClass, SubClass, subMethods){
        Bridge.prototype = SuperClass.prototype;
        SubClass.prototype = new Bridge();
        if(subMethods){
            for(var method in subMethods){
                SubClass.prototype[method] = subMethods[method];
            }
        }
        Object.freeze(SubClass.prototype);
        return SubClass;
    };
})();

IIFE 내부에서 Bridge를 선언하여 클로저로 활용했다.

예제 3 - Object.create 활용

ES5에서 도입된 Object.create를 이용하여 SubClass.prototype__proto__SuperClass.prototype을 바라보되 SuperClass의 인스턴스가 되지 않고 간단하며 안전하다.

// ...
Square.prototype = Object.create(Rectangle.prototype);
Object.freeze(Square.prototype);
// ...

결국 SubClass.prototype__proto__SuperClass.prototype을 참조하고, SubClass.prototype에 불필요한 프로퍼티가 남아있지 않으면 되는 것이다.


2.3. constructor 복구하기

2.2에서 다룬 방법에서 모두 기본적인 상속에는 성공했으나 SubClass 인스턴스의 constructor는 여전히 SuperClass를 가리키는 상태이다. 따라서 2.2 코드의 SubClass.prototype.constructor가 원래의 SubClass를 바라보도록 하는 방법을 살펴본다.

예제 1 - 인스턴스 생성 후 프로퍼티 제거

var extendClass1 = function(SuperClass, SubClass, subMethods){
    SubClass.prototype = new SuperClass();
    for(var prop in SubClass.prototype){
        if(SubClass.prototype.hasOwnProperty(prop)){
            delete SubClass.prototype[prop];
        }
    }
    // constructor 복구
    SubClass.prototype.constructor = SubClass;
    if(subMethods){
        for(var method in subMethods){
            SubClass.prototype[method] = subMethods[method];
        }
    }
    Object.freez(SubClass.prototype);
    return SubClass;
};

예제 2 - 빈 함수를 이용

var extendClass2 = (function(){
    var Bridge = function(){};
    return function(SuperClass, SubClass, subMethods){
        Bridge.prototype = SuperClass.prototype;
        SubClass.prototype = new Bridge();
        // constructor 복구
        SubClass.prototype.constructor = SubClass;
        Bridge.prototype.constructor = SuperClass;
        // SubClass.prototype이 Bridge의 인스턴스를 바라보는 상태이므로, SuperClass와 관계를 복구하기위해
        // Bridge.prototype.constructor 가 SuperClass를 바라보게 하는 작업을 추가해야함.
        if(subMethods){
            for(var method in subMethods){
                SubClass.prototype[method] = subMethods[method];
            }
        }
        Object.freeze(SubClass.prototype);
        return SubClass;
    };
})();

예제 3 - Object.create 활용

var extendClass3 = function(SuperClass, SubClass, subMethods){
    SubClass.prototype = Object.create(SuperClass.prototype);
    // constructor 복구
    SubClass.prototype.constructor = SubClass;
    if(subMethods){
            for(var method in subMethods){
                SubClass.prototype[method] = subMethods[method];
            }
        }
        Object.freeze(SubClass.prototype);
        return SubClass;
};

2.4. 상위 클래스에서 접근 수단 제공

하위 클래스에서 상위 클래스 메서드 실행 결과를 바탕으로 추가 작업을 하고 싶을 때는 SuperClass.prototype.method.apply(this, arguments)로 실행 하면 되지만, 가독성을 높여 super 를 흉내 내보고자 한다.

var extendClass = function(SuperClass, SubClass, subMethod){
    SubClass.prototype = Object.create(SuperClass.prototype);
    SubClass.prototype.constructor = SubClass;
    // super method 추가
    SubClass.prototype.super = function(propName){
        var self = this;
        if(!propName)   return function(){
            // 인자가 비어있을 경우 생성자 함수에 접근
            SuperClass.apply(self, arguements);
        }
        var prop = SuperClass.prototype[propName];
        // 함수가 아닌 경우 값에 접근
        if(typeof prop !== 'function')  return prop;
        return function(){
            return prop.apply(self, arguments);
        }
    };

    if(subMethods){
        for(var method in subMethods){
            SubClass.prototype[method] = subMethods[method];
        }
    }
    Object.freeze(SubClass.prototype);
    return SubClass;
};

var Rectangle = function(width, height){
    this.width = width;
    this.height = height;
};
Rectangle.prototype.getArea = function(){
    return this.width * this.height;
};
var Square = extendClass(
    Rectangle,
    function(width){
        this.super()(width, width); // super 사용
    },{
        getArea: function(){
            console.log('size is : ', this.super('getArea')()); // super 사용
        }
    }
);
var sq = new Square(10);
sq.getArea();   // size is : 100
console.log(sq.super('getArea')());    // 100

3. ES6 클래스와 상속

ES5와 문법비교

var ES5 = function(name){
    this.name = name;
};
ES5.staticMethod = function(){
    return this.name + ' staticMethod';
};
ES5.prototype.method = function(){
    return this.name + ' method';
};
var es5Instance = new ES5('es5');
console.log(ES5.staticMethod());      // ES5 StaticMethod
console.log(es5Instance.method());    // es5 method

var ES6 = class {
    constructor(name){
        this.name = name;
    }
    static staticMethod(){
        return this.name + ' staticMethod';
    }
    method(){
        return this.name + ' method';
    }
};
var es6Instance = new ES6('es6');
console.log(ES6.staticMethod());      // ES6 StaticMethod
console.log(es6Instance.method());    // es6 method

ES6의 클래스 상속

var Rectangle = class{
    construtor(width, height){
        this.width = width;
        this.height = height;
    }
    getArea(){
        return this.width * this.height;
    }
};
var Square = class.extends.Rectangle{   // 상속관계 설정
    cunstructor(width){
        super(width, width);    // SuperClass의 constructor 실행
    }
    getArea(){
        console.log('size is : ', super.getArea());
        // SuperClass.prototype을 바라보며 this는 원래의 this를 따름.
    }
}

정리

  • 자바스크립트는 클래스 및 상속 개념이 존재하지 않으나, 프로토타입을 기반으로 클래스와 비슷하게 동작하게 하는 기법들이 존재한다.
  • 클래스의 prototype 내부에 정의된 메서드를 프로토타입 메서드라고 하며, 인스턴스에서 호출할 수 있다.
  • 클래스 생성자 함수에 직접 정의한 메서드는 스태틱 메서드이며, 클래스에 의해서만 호출할 수 있다.

  • 클래스 상속을 흉내내기 위한 세 가지 방법
      1. SubClass.prototypeSuperClass의 인스턴스를 할당한 후 프로퍼티를 삭제
      1. 빈 함수(Bridge)를 활용
      1. Object.create를 이용하는 방법
    • 세 방법은 모두 constructor 프로퍼티가 원래 생성자 함수를 바라보도록 조정해야한다.
  • ES6에서는 class.extends 를 통해 간단하게 상속 및 추상화를 구현한다.

Comments