Javascript는 프로토타입 기반 언어이기 때문에 "상속"이라는 개념이 존재하지 않지만 사용자의 니즈에 따라 결국 ES6에는 클래스 문법이 추가되었습니다. 다만 ES6의 클래스에서도 프로토타입을 활용하고 있기에 ES5 체제 하에서 클래스를 흉내내기 위한 구현 방식을 학습하는 것은 여전히 큰 의미가 있습니다.
클래스(class)는 단어의 의미와 동일하게 "집단"이라는 의미로 접근하면 됩니다. 집단은 "어떤 기준을 충족하는 것들의 모임"이며 어떤 기준이란 추상적일 수도, 구체적일 수도 있습니다. 그리고 그 기준을 충족하는 것을 인스턴스(instance)라고 부릅니다.
예를 들면 필자는 대구에서 태어났으며 서울에서 거주 중인 분가한 불효자라는 여러 분류에 속합니다. (부모님 사랑합니다!) 이는 이미 존재하는 필자를 성질에 따라 분류해서 다양한 클래스가 생성이 된 것이죠. 즉, 현실세계에선 개체들이 이미 존재하는 상태에서 이들을 구분짓기 위해 클래스를 도입했습니다.
반면 프로그래밍 언어상에서는 접근 방식이 정반대입니다. 사용자가 직접 여러 가지 클래스를 정의한 후에 이를 바탕으로 인스턴스를 만들 때 비로소 개체가 클래스의 속성을 지니게 되는 것입니다. 또한 하나의 인스턴스는 하나의 클래스만을 바탕으로 만들어집니다. 다중상속을 지원한다 할지라도 인스턴스를 생성할 때 호출할 수 있는 클래스는 오직 하나뿐일 수 밖에 없기 때문입니다.
Javascript는 프로토타입 기반 언어이기 때문에 클래스의 개념이 존재하지 않습니다. 하지만 클래스와 비슷한 성질을 가지고 있는 곳도 있습니다. 이전 게시글에서 설명했던 prototype
이 그렇습니다.
생성자 함수 Array
를 new
연산자와 함께 호출하면 인스턴스가 생성됩니다. 이때 Array
를 일종의 클래스라고 하면, Array
의 prototype
객체 내부 요소들이 인스턴스에 "상속"된다고 볼 수 있습니다. Array
의 내부 프로퍼티들 중 prototype
를 제외한 나머지는 인스턴스에 상속되지 않습니다.
이렇게 인스턴스 상속 여부에 따라 다른 언어들에서는 스태틱 멤버, 인스턴스 멤버로 나뉘며 Javascript 에서는 스태틱 메서드, (프로토타입) 메서드라고 부릅니다.
var Rectangle = function (width, height) { // 생성자
this.width = width;
this.height = height;
};
Rectangle.prototype.getArea = function () { // (프로토타입) 메서드
return this.width * this.height;
};
Rectangle.isRectangle = function () { // 스태틱 메서드
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)); // TypeError: rect1.isRectangle is not a function
console.log(Rectangle.isRectangle(rect1)); // true
Javascript에서 클래스 상속을 구현했따는 것은 결국 프로토타입 체이닝을 잘 연결한 것으로 이해하면 됩니다. 위 Rectangle
객체를 상속하는 Square
객체를 만들어봅시다. (실제로 정사각형은 직사각형의 subclass
입니다.)
var Square = function (width) {
Rectangle.call(this, width, width);
};
Square.prototype = new Rectangle();
var sq = Square(5);
기본적으로는 위와 같이 구현이 가능하지만 몇가지 문제점들이 아직 존재합니다. sq.constructor
가 Square
가 아닌 Rectangle
을 바라보고 있습니다. 해당 문제는 단순히 constructor
에 Rectangle
을 할당하면 되는 부분이기에 추가적인 설명은 하지 않고 넘어가겠습니다.
...
Square.prototype = new Rectangle();
Square.prototype.constructor = Square;
...
또 다른 문제로는 불필요한 값이 존재하는 문제입니다. Rectangle
의 인스턴스를 Square.prototype
에 할당했기 때문에 아래와 같이 Square.prototype
에 값이 존재하게 됩니다.
console.log(Square.prototype)
// Rectangle {
// height: undefined
// width: undefined
// }
이후에 임의로 sq.width
값을 제거하고 Square.prototype.width
에 값을 부여하게 된다면 프로포타입 체이닝에 의해 예상치 못한 값이 출력될 수 있습니다. 이렇게 클래스에 있는 값이 인스턴스의 동작에 영향을 주어선 안 됩니다. 이는 클래스의 추상성을 해칠 뿐더러 예기치 않은 오류가 발생한 가능성을 안고 가야합니다.
쉽게 이해하기 위해 상위 객체(Rectangle
)를 superClass
, 하위 객체(Square
)를 subClass
라 지칭하겠습니다.
단순하게 생각하면 클래스에 있는 값(프로퍼티)들을 모두 제거하고 새로운 프로퍼티를 추가할 수 없게 처리할 수 있을 것입니다.
delete Square.prototype.width;
delete Square.prototype.height;
Object.freeze(Square.prototype);
또 다른 방법으로는 superClass
의 prototype
을 참조하는 빈 함수를 만들어 subClass
의 prototype
을 연결하는 역할을 하는 Bridge
함수를 만드는 것입니다.
var Bridge = function () {};
Bridge.prototype = Rectangle.prototype;
Square.prototype = new Bridge;
Object.freeze(Square.prototype);
마지막 방법으로는 ES5에서 도입된 Object.create
를 이용한 방법입니다.
Square.prototype = Object.create(Rectangle.prototype);
Object.freeze(Square.prototype);
하위 클래스의 메서드에서 상위 클래스의 메서드 실행 결과를 바탕으로 추가적인 작업을 수행하고 싶을 때가 있습니다. 즉, subClass
에서 superClass
의 prototype
메서드에 접근하기 위한 별도의 수단, super
을 구현해보겠습니다.
Square.prototype.super = function (propName) {
var self = this;
if (!propName)
return function () {
Rectangle.apply(self, arguments);
};
var prop = Rectangle.prototype[propName];
if (typeof prop !== "function") return prop;
return function () {
return prop.apply(self, arguments);
};
};
Square.prototype.getArea = function () {
return "size is: " + this.super("getArea")();
};
Object.freeze(Square.prototype);
var sq = new Square(5);
console.log(sq.getArea()); // size is: 25
console.log(sq.super("getArea")()); // 25
위 Rectangle
과 Square
을 ES6의 class
문법으로 구성하면 아래와 같습니다.
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 () {
return "size is: " + super.getArea();
}
}