클래스는 공통 요소를 지니는 집단을 분류하기 위한 개념으로,
객체 지향 프로그래밍에서 특정 객체를 생성하기 위해 변수와 메소드를 정의하는 일종의 틀이다.
자바스크립트는 객체 지향 프로그래밍을 지원하지만
다른 언어와 달리 프로토타입 기반 언어이며 상속 개념이 존재하지 않는다.
(ES6부터 클래스 문법이 추가되었다.)
ES5에는 클래스의 개념이 존재하지 않았지만
프로토타입을 일반적인 의미에서의 클래스 관점에서 접근해보면
비슷하게 해석할 수 있는 요소가 있다.
생성자 함수의 프로토타입 객체 내부 메소드들은
프로토타입 체이닝에 의해 __proto__
을 생략하고 참조하여
인스턴스에서 마치 자신의 멤버인 것처럼 사용할 수 있다.
이는 인스턴스에서 프로토타입의 메소드를 상속받았다고 볼 수 있다.
생성자 함수를 new
키워드로 호출하면 인스턴스가 생성된다.
이때 인스턴스는 스태틱 멤버와 프로토타입 멤버를 갖게 된다.
아래의 예제를 살펴보자!
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
Rectangle
이라는 이름의 생성자 함수를 정의하였다.
이 Rectangle
의 prototype
에 getArea
메소드를 정의하였고,
생성자 함수인 Rectangle
에 바로 isRectangle
메소드를 정의하였다.
결론부터 말하자면 getArea
은 프로토타입 멤버이며 isRectangle
은 스태틱 멤버이다.
프로토타입 멤버는 인스턴스에서 __proto__
을 생략하고 마치 자신이 상속받은 것처럼 사용할 수 있다.
그러나 스태틱 멤버는 인스턴스에서 직접 호출할 수 없으며
생성자 함수 자체를 this
로 바인딩하여 호출할 수 있다.
따라서 위 예제 코드에서 rect1.isRectangle(rect1)
는 에러가 발생하게 된다.
인스턴스에서 직접 스태틱 멤버를 호출했기 때문이다.
var Rectangle = function (width, height) {
this.width = width;
this.height = height;
// 인스턴스 멤버
this.print = function () {
console.log('사각형입니다.');
};
};
// 프로토타입 멤버
Rectangle.prototype.getArea = function () {
return this.width * this.height;
};
// 스태틱 멤버
Rectangle.isRectangle = function (instance) {
return instance instanceof Rectangle &&
instance.width > 0 && instance.height > 0;
};
생성자 함수 내에서 this
키워드를 사용하여 정의되는 멤버는 인스턴스 멤버이다.
위 예제에서는 this.width
, this.height
, this.print
메소드가 해당된다.
생성자 함수의 인스턴스가 생성될 때마다 해당 인스턴스에 대해 고유하게 속성과 메소드가 생성되며
별도의 메모리를 차지하기 때문에 많은 인스턴스를 생성할 경우 비효율적이다.
생성자 함수의 prototype
속성을 통해 정의되는 멤버는 프로토타입 멤버이다.
위 예제에서 getArea
메소드에 해당한다.
모든 인스턴스는 동일한 프로토타입 객체를 공유하며 프로토타입 체이닝을 통해 접근할 수 있다.
메소드가 여러 번 정의되지 않기 때문에 메모리 사용 측면에서 효율적이다.
ES6에 등장한 클래스에서 일정 부분은 사실 프로토타입을 활용하고 있는 것이기 때문에
ES5 체제 하에서 프로토타입 체인을 이용해 클래스 상속을 구현하는 방식을 공부해두면
JS의 클래스에 대해서도 잘 이해할 수 있다.
프로토타입 체인을 이용해 클래스를 흉내내기 위한 방법으로는 크게 3가지가 존재한다.
① SuperClass
의 인스턴스를 SubClass.prototype
에 할당
② Bridge
활용
③ Object.create
이용
위의 방법을 차례대로 살펴보도록 하자.
SuperClass
의 인스턴스를 SubClass.prototype
에 할당하고,
이 인스턴스의 프로퍼티를 모두 지운 다음에 더는 새로운 프로퍼티를 추가할 수 없게 함으로써 상속을 구현할 수 있다.
var extendClass1 = function (SuperClass, SubClass, subMethods) {
SubClass.prototype = new SuperClass();
SubClass.prototype.constructor = SubClass; // constructor 복구
for (var prop in SubClass.prototype) {
if (SubClass.prototype.hasOwnProperty(prop)) {
delete SubClass.prototype[prop];
}
}
// subMethods가 제공되면 추가
if (subMethods) {
for (var method in subMethods) {
SubClass.prototype[method] = subMethods[method];
}
}
// 프로토타입 동결
Object.freeze(SubClass.prototype);
return SubClass;
};
// 사용 예시
var Square = extendClass1(
Rectangle,
function (width) {
Rectangle.call(this, width, width);
}, { // getArea 메소드를 prototype 객체 안에 정의
getArea: function () {
return this.width * this.height;
}
}
);
이 방법은 더글라스 크락포드가 제시해서 대중적으로 널리 알려진 방법이다.
SubClass
에 직접 SuperClass
의 인스턴스를 할당하는 대신
아무런 프로퍼티도 생성하지 않는 빈 생성자 함수(Bridge
)를 하나 더 만들어서
그 prototype
이 SuperClass
의 prototype
을 바라보게끔 한 다음,
SubClass
의 prototype
에는 Bridge
의 인스턴스를 할당하게 하는 것이다.
즉, 빈 함수에 다리 역할을 부여하는 것과 같다.
var extendClass2 = (function () { // 즉시 실행 함수
var Bridge = function () {};
return function (SuperClass, SubClass, subMethods) {
Bridge.prototype = SuperClass.prototype;
SubClass.prototype = new Bridge();
SubClass.prototype.constructor = SubClass; // constructor 복구
// subMethods가 제공되면 추가
if (subMethods) {
for (var method in subMethods) {
SubClass.prototype[method] = subMethods[method];
}
}
// 프로토타입 동결
Object.freeze(SubClass.prototype);
return SubClass;
};
}) ();
이처럼 즉시 실행 함수 내부에서 Bridge
을 선언해서 이를 클로저로 활용함으로써 메모리에 불필요한 함수 선언을 줄였다.
Object.create
을 이용하면 SubClass
의 prototype
의 __proto__
가
SuperClass
의 prototype
을 바라보되 SuperClass
의 인스턴스가 되지 않는다.
앞서 소개한 두 방법보다 훨씬 간단하면서 안전하다.
var extendClass3 = function (SuperClass, SubClass, subMethods) {
SubClass.prototype = Object.create(SuperClass.prototype);
SubClass.prototype.constructor = SubClass; // constructor 복구
// subMethods가 제공되면 추가
if (subMethods) {
for (var method in subMethods) {
SubClass.prototype[method] = subMethods[method];
}
}
// 프로토타입 동결
Object.freeze(SubClass.prototype);
return SubClass;
};
ES6에서 등장한 클래스 문법을 사용하여 상속을 구현하면 다음과 같다.
extends
키워드를 이용하면 쉽게 Square
가 Rectangle
을 상속받도록 할 수 있다.
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());
}
};