본문으로 바로가기

7장 클래스

category 코어 자바스크립트 2020. 12. 28. 16:35

자바스크립트는 프로토타입 기반 언어라서 '상속' 개념이 존재하지 않는다

클래스 기반의 다른 언어에 익숙한 많은 개발자들은 이를 혼란스러워 했고, 따라서 클래스와 비슷하게 동작하게끔 흉내 내는 여러 기법들이 탄생 했으며 결국 ES6에는 클래스 문법이 추가되었다

 

클래스와 인스턴스의 개념 이해

프로그래밍 언어에서의 클래스는 현실세계에서의 클래스와 마찬가지로 '공통요소를 지니는 집단을 분류하기 위한 개념'이라는 측면에서는 일치하지만 인스턴스들로부터 공통점을 발견해서 클래스를 정의하는 현실과 달리, 클래스가 먼저 정의돼야만 그로부터 공통적인 요소를 지니는 개체들을 생성할 수 있다

 

클래스간의 상하관계

위의 그림에서 음식, 과일, 귤류는 모두 클래스이다

음식은 과일보다 상위의 (superior) 개념이고, 과일은 음식보다 하위의 (subordinate) 개념이다 

 

한편 음식은 귤류의 super-superclass 이고

귤류는 음식의 sub-subclass 이다

이처럼 클래스는 하위로 갈수록 상위 클래스의 속성을 상속하면서 더 구체적인 요건이 추가 또는 변경된다

 

 

접근 방식

프로그래밍 언어와 현실세계에서의 클래스는 접근 방식이 반대이다

 

현실세계 프로그래밍 언어
나를 주체로 남성, 직장인, 한국인 이라는 분류가 생성된다 위와 같은 구분법을 알지 못하므로 사용자가 직접 여러가지 클래스를 정의해서 그 클래스를 바탕으로 인스턴스를 만들 때 비로소 어떤 개체가 클래스의 속성을 지니게된다 

또한 프로그래밍 언어에서 인스턴스는 하나의 클래스만을 바탕으로 만들어진다, 어떤 인스턴스가 다양한 클래스에 속할 수 있지만 이 클래스들은 모두 인스턴스 입장에서는 '직계존속'이다 

다중 상속을 지원하는 언어이든 그렇지 않은 언어이든 결국 인스턴스를 생성할 때 호출할 수 있는 클래스는 오직 하나뿐일 수 밖에 없기 때문이다

 

 

자바스크립트의 클래스

인스턴스에 상속되는지(인스턴스가 참조하는지) 여부에 따라 스태틱 멤버와 인스턴스 멤버로 나뉜다

여느 클래스 기반 언어와 달리 자바스크립트에서는 인스턴스에서도 직접 메서드를 정의할 수 있기 때문에 '인스턴스 메서드' 라는 명칭보다는 '프로토타입 메서드' 라고 부르고 있다

 

스태틱 메서드, 프로토타입 메서드

var Rectangle = function (width, height) {    // 생성자
    this.width = width;
    this.height = height;
};

Rectangle.prototype.getArea = function () {    // 프로토타입 메서드
    return this.width * this.height;
};

Rectangle.isRectangle = function (instance) {    // 스태틱 메서드
    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.isRectangle(rect1) 처럼 인스턴스에서 isRectangle에 접근 하려고 할때 에러가 나는 이유는

rect1.__proto__와 rect1.__proto__.__proto__에서 검색을 했는데도 해당 메서드를 찾지 못해서 Error가 나왔다 

이렇게 인스턴스에서 직접 접근할 수 없는 메서드를 스태틱 메서드라고 한다

스태틱 메서드는 생성자 함수를 this로 해야만 호출할 수 있다

 

보통 클래스는 구체적인 인스턴스가 사용할 메서드를 정의한 '틀'의 역할을 담당하는 목적을 가진 추상적인 개념이지만

이처럼 스태틱 메서드를 호출할 때의 클래스는 그 자체가 하나의 개체로서 취급된다

 

 

클래스 상속

자바스크립트에서는 프로토 체인을 활용해 클래스 상속을 구현하는 것처럼 흉내를 내서 객체지향 언어의 클래스와 비슷한 형태로까지 발전시켰다

 

Rectangle 클래스와 Square 클래스

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;
};
Square.prototype.getArea = function () {
    return this.width * this.width;
};

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

위의 코드에서 Rectangle과 Square 클래스에 공통요소가 보인다

Square에서 width 프로퍼티만 쓰지 않고 heigth 프로퍼티에 width 값을 부여하는 형태가 된다면 getArea를 동일 하게 사용할 수 있을 것 같다

 

Square클래스의 변형

var Square = function (width) {
    this.width = width;
    this.heigth = width;
};

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

Square클래스를 변형 시키고나니

소스 상으로도 Square를 Rectangle의 하위 클래스로 삼을 수 있을 것 같다

getArea라는 메서드는 동일한 동작을 하므로 상위 클래스에서만 정의하고, 하위클래스에서는 해당 메서드를 상속하면서 height 대신 width를 넣어주면 될 것 같다

 

Rectangle을 상속하는 Square 클래스

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

Square의 생성자 함수 내부에서 Rectangle의 생성자 함수를 함수로써 호출했다

이때 인자 height 자리에 width 를 전달했다

그리고 메서드를 상속하기 위해 Square의 프로토 타입 객체에 Rectangle의 인스턴스를 부여했다

 

일단 원하는대로 동작하긴 한다

하지만 위의 코드가 완벽한 클래스 체계를 구축했다고 볼 수 는 없다

아직 클래스에 있는 값이 인스턴스에 영향을 줄 수 있는 구조라는 문제를 가지고 있다

 

만약 이후에 임의로 Square.prototype.width에 값을 부여하고 sq.width의 값을 지워버린다면 프로토타입 체이닝에 의해 엉뚱한 결과가 나오게 될 것이다

 

그리고 constructor가 여전히 Rctangle을 바라보고 있으니 이것도 문제가 된다

 

다음 절에서 이런 문제들을 해결해보자

 

 

클래스가 구체적인 데이터를 지니지 않게 하는 방법

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

클래스가 구체적인 데이터를 지니지 않게 하는방법은 여러 가지가 있는데, 그중 가장 쉬운 방법은 일단 만들고 나서 프로퍼티들을 일일이 지우고 더는 새로운 프로퍼티를 추가할 수 없게 하는 것이다

 

 

두번째 다른 방안으로는 더글라스 크락포드가 제시해서 대중적으로 알려진 방법이 있다

var Rectangle = function (width, height) {
    this.width = width;
    this.heigth = heigth;
};
Rectangle.prototype.getArea = function () {
    return this.width * this.height;
};
var Square = function (width) {
    Rectangle.call(this, width, width);
};

var Bridge = function () {};
Bridge.prototype = Rectangle.prototype;
Square.prototype = new Bridge();
Object.freeze(Square.prototype);

 

Bridge라는 빈 함수를 만들고, Bridge.prototype이 Rectangle.prototype을 참조하게 한 다음, Square.prototype에 new Bridge()로 할당하면 위의 그림처럼 Rectangle 자리에 Bridge가 대체하게 될 것이다

이로써 인스턴스를 제외한 프로토타입 체인 경로상에는 더는 구체적인 데이터가 남아있지 않게 된다

 

 

위의 코드를 범용성을 고려해서 함수로 만들어보자

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

 

ES5에서 도입된 Object.create를 이용한 방법도 있다

이 방법은 SubClass의 prototype의 __proto__가 SuperClass의 prototype을 바라보되, SuperClass의 인스턴스가 되지는 않으므로 앞서 소개한 방법보다 간단하면서 안전하다

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

 

 

constructor 복구하기

위의 방법은 기본적인 상속에는 성공했지만 SubClass 인스턴스의 constructor는 여전히 SuperClass를 가리키는 상태이다, 엄밀히는 SubClass 인스턴스에는 constructor가 없고, SubClass.prototype에도 없는 상태이다

SuperClass.prototype의 construcotr에서 가리키는 대상은 SuperClass이다

따라서 SubClass.prototype.constructor가 SubClass를 바라보도록 해야한다

 

Object.create활용해서 클래스 상속 및 추상화

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

 

 

ES6의 클래스 및 클래스 상속

ES5와 ES6의 클래스 문법 비교

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 {
    constructor (width, height) {
        this.width = width;
        this.height = height;
    }
    getArea () {
        return this.width * this.height;
    }
};

var Square = class extends Rectangle {
    constructor (width) {
        super(width, width);
    }
    getArea () {
        console.log('size is :', super.getArea());
    }
};
  • class 명령어 뒤에 'extends Rectangle'이라는 내용을 추가 했는데 이것만으로 상속관계 설정이 끝난다
  • constructor 내부에서는 super 라는 키워드를 함수처럼 사용할 수 있는데, 이 함수는 SuperClass의 constructor를 실행한다
  • constructor 메서드를 제외한 다른 메서드에서는 super 키워드를 마치 객체처럼 활용할 수 있고, 이때 객체는 SuperClass.prototype을 바라보는데, 호출한 메서드의 this는 'super'가 아닌 원래의 this를 그대로 따른다

'코어 자바스크립트' 카테고리의 다른 글

6장 프로토타입  (2) 2020.12.24
5장 클로저  (0) 2020.12.23
4장 콜백함수  (0) 2020.12.22
3장 this  (2) 2020.12.21
1장 메모리와 데이터 타입  (0) 2020.12.21
2장 실행 컨텍스트  (0) 2020.12.21