자바스크립트는 프로토타입 기반 언어라서 ‘상속’개념이 존재하지 않습니다!
그래서 다른언어에 익숙한 개발자들을 혼란스럽게 만들었고
이를 해결하기 위해 ES6에서 클래스 문법이 추가 되었습니다.
하지만! 이부분도 프로토타입을 활용하여 구현한것입니다!
그래서 이번 정리는 클래스가 어떤식으로 구현이 되었는지 한번 확인해 보겠습니다!
예를들어 생성자 함수 Array를 new를 통해 arr라는 인스턴스를 만들었다고 생각해 보겠습니다.
Array 를 일종의 class로 본다면 Array의 prototype 객체 내부 요소들이 arr로 상속된다고 보는 것입니다. 물론 프로토타입 체이닝에 의한 참조입니다.Array의 prototype을 제외한 메서드는 arr에 상속되지 않습니다.또 다른 예제를 통해 확인 해보겠습니다.
// 생성자
const 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;
};
const rect1 = new Rectangle(3,4);
console.log(rect1.getArea()); // 12
console.log(rect1.isRectangle()); // TypeError: rect1.isRectangle is not a function
console.log(Rectangle.isRectangle(rect1)); // true
rect1에서 프로토타입 체이닝을 통해 getAreat()를 바로 호출할 수 있는 메서드를 프로토타입 메서드입니다.rect1에서 프로토타입 체이닝으로 직접 접근할 수 없는 메서드 isRectangle()가 스태틱 메서드 입니다.그래서 ES5에서는 클래스가 없기 때문에 이런 프로토타입 체이닝을 통해 클래스 상속을 구현한 것입니다.
직접 확인하면서 클래스를 직접 구현했을때 어떤 문제가 있는지 확인해 보겠습니다.
const Grade = function() {
const args = Array.prototype.slice.call(arguments);
for (var i = 0; i < args.length; i++) {
this[i] = args[i];
}
this.length = args.length;
};
Grade.prototype = [];
const g = new Grade(100, 80);
g.push(90);
console.log(g); // Grade { 0: 100, 1: 80, 2: 90, length: 3 }
delete g.length;
g.push(70);
console.log(g); // Grade { 0: 70, 1: 80, 2: 90, length: 1 }
push 한것은 정확하게 나왔지만length 속성이 수정 가능한 점과 Grade.prototype가 빈배열을 참조 한다는 점이 문제가 발생하게 된 원인입니다.length 속성이 삭제가 안되지만 삭제가 가능하게 되어 g.length 읽어올라 했지만 사라져서 프로토타입 체이닝을 통해 g.__proto__.length를 읽어 왔기때문에 빈배열(Grade.prototype)의 length 0을 읽게되 잘못되게 push가 된 것 입니다.이렇기 때문에 클래스에 있는 값이 인스턴스의 동작에 영향을 미치면 안됩니다!!
또다른 예제를 확인해 보겠습니다.
const Rectangle = function (width, height) {
this.width = width
this.height = height
}
Rectangle.prototype.getArea = function () {
return this.width * this.height
}
const rect = new Rectangle(3,4);
console.log(rect.getArea()); // 12
const Square = function (width) {
Rectangle.call(this, width, width)
}
Square.prototype = new Rectangle()
const sq = new Squeare(5);
console.log(sq.getArea()); // 25
다음과 같이 직사각형과 정사각형 클래스를 만들어서 공통된 메서드를 사용하기 위해 Squre 의 프로토타입 객체에 Rectangle의 인스턴스를 부여했습니다. (마치 클래스 상속)
물론 동작은 제대로 하지만 문제점이 있습니다!
클래스의 값때문에 인스턴스에 영향이 미치는것을 확인할 수 있습니다!
sq의 구조를 보게 되면sq.__proto__는 Rectengle의 인스턴스를 바라보게 되어
width와 height가 undefined로 되어있는것을 확인 할 수 있습니다.
즉Square.prototype에 값이 존재 하여 임의로Square.prototype.width에 값을 부여하여sq.width를 지워 버리면 프로토타입 체이닝에 의해 이상한 값이 나오게 될 것입니다!
그렇기 때문에 이러한 문제를 해결하기 위해 클래스가 구체적인 데이터를 지니지 않게 해야합니다!
const extendClass1 = function(SuperClass, SubClass, subMethods) {
SubClass.prototype = new SuperClass();
for (const prop in SubClass.prototype) {
if (SubClass.prototype.hasOwnProperty(prop)) {
delete SubClass.prototype[prop];
}
}
if (subMethods) {
for (const method in subMethods) {
SubClass.prototype[method] = subMethods[method];
}
}
Object.freeze(SubClass.prototype);
return SubClass;
};
const Rectangle = function(width, height) {
this.width = width;
this.height = height;
};
Rectangle.prototype.getArea = function() {
return this.width * this.height;
};
const Square = extendClass1(Rectangle, function(width) {
Rectangle.call(this, width, width);
});
const sq = new Square(5);
console.log(sq.getArea());
간단하게 설명하자면
extendClass1에서 SuperClass의 인스턴스를 SubClass.prototype으로 설정하여 상속을 구현했습니다. 이후 SubClass.prototype 에 직접 정의된 프로퍼티를 삭제하여 subMethods 객체를 받아 SubClass.prototype에 원하는 메서드를 추가합니다. 마지막으로 Object.freeze(SubClass.prototype)를 사용하여 SubClass.prototype을 변경할 수 없도록 만들었습니다.const extendClass2 = (function() {
const Bridge = function() {};
return function(SuperClass, SubClass, subMethods) {
Bridge.prototype = SuperClass.prototype;
SubClass.prototype = new Bridge();
if (subMethods) {
for (const method in subMethods) {
SubClass.prototype[method] = subMethods[method];
}
}
Object.freeze(SubClass.prototype);
return SubClass;
};
})();
const Rectangle = function(width, height) {
this.width = width;
this.height = height;
};
Rectangle.prototype.getArea = function() {
return this.width * this.height;
};
const Square = extendClass2(Rectangle, function(width) {
Rectangle.call(this, width, width);
});
const sq = new Square(5);
console.log(sq.getArea());
Bridge라는 빈 함수를 만들어서 Bridge.prototype이 Rectangle.prototype을 바라보게끔 한 다음 Square.prototype에 Bridge의 인스턴스를 할당하여Rectangle 자리에 Bridge가 대체하게 되어 구체적인 데이터가 남아 있지 않습니다.const Rectangle = function(width, height) {
this.width = width;
this.height = height;
};
Rectangle.prototype.getArea = function() {
return this.width * this.height;
};
const Square = function(width) {
Rectangle.call(this, width, width);
};
Square.prototype = Object.create(Rectangle.prototype);
Object.freeze(Square.prototype);
const sq = new Square(5);
console.log(sq.getArea());
SubClass의 prototype의 __proto__가 SuperClass의 prototype을 바라보되SuperClass의 인스턴스가 되지 않아서 해결하는 방법입니다.SubClass 인스턴스의 constructor는 SuperClass를 가르키는 상태입니다. 즉 constructor가 다른 부분을 가르키고 있기 때문에 수동으로 올바르게 연결해야 합니다.const extendClass1 = function(SuperClass, SubClass, subMethods) {
SubClass.prototype = new SuperClass();
for (const prop in SubClass.prototype) {
if (SubClass.prototype.hasOwnProperty(prop)) {
delete SubClass.prototype[prop];
}
}
SubClass.prototype.consturctor = SubClass; // 수동으로 올바르게 가르키게 변경
if (subMethods) {
for (const method in subMethods) {
SubClass.prototype[method] = subMethods[method];
}
}
Object.freeze(SubClass.prototype);
return SubClass;
};