클래스(prototype)가 구체적인 데이터를 지니지 않게 하는 방법은 여러 가지가 있는데, 그중 가장 쉬운 방법은 일단 만들고 나서 프로퍼티를 일일이 지우고 더는 새로운 프러퍼티를 추가할 수 없게 하는 것이다.
delete Square.prototype.width;
delete Square.prototype.height;
Object.freeze(Square.prototype)
Object.freeze MDN
프로퍼티가 많다면 좀 더 함수를 만들자
let extendClass = function(SuperClass, SubClass, subMethods){
SubClass.prototype = new SuperClass();
for(let prop in SubClass.prototype){
if(SubClass.prototype.hasOwnProperty(prop)){
delete SubClass.prototype.prototype[prop]
}
}
if(subMethods){
for(let method in subMethods){
SubClass.prototype[method] = subMethods[method]
Object.freeze(SubClass.prototype)
return SubClass
}
}
}
let Square = extendClass(Rectangle, function(width){
Rectangle.call(this, width, width)
})
extendClass 함수는 SuperClass, subClass에 추가할 메소드들이 정의된 객체를 받아서 subClass의 protype 내용을 정리하고 freeze하는 내용으로 구성되어있다. 좀 복잡하지만 범용성 측면에서는 괜찮은 방법이다.
두 번째로는 더글라스 크락포드가 제시해서 대중적으로 널리 알려진 방법으로, subClass의 prototype에 직접 subClass의 인스턴스를 할당하는 대신 아무런 프로퍼티를 생성하지 않는 빈 생성자 함수(Bridge)를 하나 더 만들어 그 prototype이 superClass의 prototype을 바라보게끔 한 다음, subClass의 prototype에는 Birdge의 인서턴스를 할당하게 하는 것이다. 빈 함수에 다리 역할을 부여하는 것이다..!
let Rectangle = function (width, height) {
this.width = width;
this.height = height;
};
Rectangle.prototype.getArea = function () {
return this.width * this.height;
};
let Square = function (width) {
Rectangle.call(this, width, width);
};
let 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가 대체하게 될 것이다. 이로써 인스턴스를 제외한 프로토타입 체인 경로상에는 더는 구체적인 데이터가 남아있지 않게 된다. 이걸 다시 함수로 만든다면
let extendClass2 = (function(){
let 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
}
})()
즉시살행함수 내부에서 Bridge를 선언해서 이를 클로저로 활용함으로써 메모리에게 불필요한 함수선언을 줄였다. subMethod에는 subClass의 prototype에 담길 메소드를 객체로 전달하게끔 했다.
마지막으로 ES5에서 도입된 Object.create를 이용한 방법이다. 이 방법은 subClass의 prototype의 __proto__
가 SuperClass의 prototype을 바라보되, SuperClass의 인스턴스가 되지는 않으므로 앞의 두 방법보단 간단하고 안전하다.
let extendClass3 = function(SuperClass, SubClass, subMethods){
SubClass.prototype = Object.create(SuperClass.prototype)
Object.freeze(Square.prototype)
if(subMethods){
for(let method in subMethods){
SubClass.prototype[method] = subMethods[method]
}
}
Object.freeze(SubClass.prototype)
return SubClass
}
위 세가지 방법 모드 기본적인 상속은 성공했지만 subClass의 인스턴스의 constuctor는 여전이 SuperClass를 가리키는 상태다. 엄밀히는 subClass 인스턴스에는 constuctor가 없고, subClass.prototype에도 없는 상태다. 프로토타입 체인상에서 가장 먼저 등장하는 Superclass.prototype의 constructor에서 가리키는 대상, 즉 SuperClass가 출력될 뿐이다. 따라서 위 코드들의 Subclass.prototype.constructor가 원래의 subClass를 바로보도록 해주면 된다.
let extendClass = function (SuperClass, SubClass, subMethods) {
SubClass.prototype = new SuperClass();
for (let prop in SubClass.prototype) {
if (SubClass.prototype.hasOwnProperty(prop)) {
delete SubClass.prototype.prototype[prop];
}
}
subMethods.prototype.constuctor = subClass;
if (subMethods) {
for (let method in subMethods) {
SubClass.prototype[method] = subMethods[method];
}
}
Object.freeze(SubClass.prototype);
return SubClass;
};
let extendClass2 = (function(){
let Bridge = function(){}
return function(SuperClass, SubClass, subMethods){
Bridge.prototype = SuperClass.prototype
SubClass.prototype = new Bridge()
subClass.prototype.constructor = subClass;
if(subMethods){
for(let method in subMethods){
SubClass.prototype[method] = subMethods[method]
}
}
Object.freeze(SubClass.prototype)
return SubClass
}
})()
let 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.prototype)
return SubClass
}
let ES5 = function (name) {
this.name = name;
};
ES5.staticMethod = function () {
return this.name + " staticMethod";
};
ES5.prototype.method = function () {
return this.name + " method";
};
let es5Instance = new ES5("es5");
console.log(ES5.staticMethod()); // ES5 staticMethod
console.log(es5Instance.method()); // es5 method
/**************** ES6 ****************/
let ES6 = class {
constructor(name) {
this.name = name;
}
static staticMethod() {
return this.name + " staticMethod";
}
method() {
return this.name + " method";
}
};
let es6Instance = new ES6("es6");
console.log(ES6.staticMethod()); //ES6 staticMethod
console.log(es6Instance.method()); // es6 method
이런식으로 바꾸면 된다.
class라는 명령어 뒤에는 바로 {}가 등장한다. 이 {}묶음 내부가 클래스 본문 영역이다.
클래스내에서 함수를 선언할때 function 키워들르 생략해도 모두 메소드로 인식을 한다.
constructor라는 이름에서 알 수 있듯이 생성자 함수의 역할을 한다.
메소드와 다음 메소듣 사이에는 콤마로 구분하지 않는다.
static이라는 키워드는 해당 메소드가 static메소임을 알리는 내용이고, 생성자 함수에 바로 할당하는 메소드와 동일하게 생성자 함수 자신만이 호출 할 수 있다.
이제 상속하는 법을 알아보자
let Rectangle = class {
constructor(w, h) {
this.w = w;
this.h = h;
}
getArea() {
return this.w * this.h;
}
};
let Square = class extends Rectangle {
constructor(w) {
super(w, w);
}
getArea() {
console.log("size is: ", super.getArea());
}
};
extends키워드를 이용해서 상위 클래스를 상속 받을 수 있다. 여기서는 Rectangle클래스를 Square클래스가 상속을 받는다. super키워드를 사용해서 상위 클래스의 constructor를 실행한다.
constructor메소드를 제외한 나머지 다른 메소드에서는 마치 super키워드를 객체처럼 사용이 가능한데 이때 객체는 SuperClass.prototype을 바라본다. 호출한 메소드의 this는 super가 아니라 원래의 this를 그대로 따른다..!!
JS는 프로타입기반 언어라 클래스및 상속 개념이 존재하지 않지만 비슷하게 동작하게끔 하는 많은 동작기법이 있다.
클래스는 어떤 사물의 공통 속성을 모아 정의한 추상적인 개념이고, 인스턴스는 클래스의 속성을 지니는 구체적인 시례이다. 상위클래스의 조건을 충족하면서 더욱 구체적인 조건이 추가된 것을 하위클래스라고 부른다.
클래스의 내부에 정의된 메소드를 프로토타입 메소드라고 부르고 이들은 인스턴스가 마치 자신의 것처럼 부를 수 있다. 한편 클레스에 직접 정의한 메소드를 스태틱메소드라고 하며, 이들은 인스턴스가 부를 수 없고 해당 클래스에 의해서만 부를 수 있다.