자바스크립트는 프로토타입 기반 언어라서 '상속' 개념이 존재하지 않는다.
그래서 클래스를 흉내내는 기법들이 탄생했고 ES6에는 클래스 문법이 추가되었다.
클래스 문법에서도 프로토타입을 활용하기에 ES5 체제 하에 클래스를 구현하는 방식을 학습하는 것이 의미가 있다.
자바스크립트는 프로토타입 기반 언어이므로 클래스 개념이 존재하지 않지만 비슷하게 해석할 요소가 있다.
상속
된다고 볼 수 있다(엄밀히는 상속이 아닌 프로토타입 체이닝에 의한 참조이다). Array 내부 프로퍼티는 prototype을 제외한 나머지는 인스턴스에 상속되지 않는다.인스턴스 메서드
라는 명칭은 프로토 타입에 정의 한 메서드를 지칭하는 것인지 인스턴스에 정의한 메서드를 지칭하는 것인지 혼란을 야기한다. 그래서 프로토타입 메서드
라고 부르는 편이 좋다(실제로 커뮤니티에서는 후자를 더 많이 사용한다.). var Person = function(name){
this._name = name;
}
Person.prototype.getName = function() {
return this._name;
} // 프로토 타입에 정의한 메서드 (프로토타입 메서드)
Person.isPerson = function(inst){
return inst instanceof Person;
} // 스태틱 메서드
var p1 = new Person('kim');
p1.getName = function(){
return this._name + '입니다.';
} // 인스턴스에서 정의한 메서드
클래스는 추상적일 수도 있고 구체적인 개체가 될 수도 있다.
- 인스턴스를 생성할 때의 틀 역할 담당하는 목적을 가질 때의 클래스는추상적 개념
- 클래스 자체를 this로 해서 직접 접근해야하는 스태틱 메서드를 호출할 때의 클래스는구체적 개체
ES5까지의 자바스크립트 커뮤니티에서는 클래스 상속을 다른 클래스 기반 언어를 흉내내는 것이 주요 관심사였다.
아래 코드가 클래스 상속의 핵심이다. Grade ↔ Array ↔ Object간의 Superclass, Subclass관계를 가진다.
var Grade = function(){
var args = Array.prototype.slice.call(arguments);
for(var i = 0; i < args.length; i++){
this[i] = args[i];
}
this.length = args.length;
}
Grade.prototype = [];
var g = new Grade(100, 80);
g.push(90);
cosole.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}
첫 번째 출력은 결과가 잘 출력되었다.
length 프로퍼티를 삭제한 후, 두번 째 출력에서는 0번 인덱스에 70이라는 값이 들어갔고 length가 1이 되었다.
이 이유는 g.length 프로퍼티가 없기 때문에 프로토타입 체이닝을 통해 따라가다가 빈 배열의 인스턴스를 가리키고 있는 g.__proto__
의 length: 0을 읽어온다. 그래서 0번 인덱스에 70을 push하고 length를 1로 늘리게되어 두 번째 출력과 같은 결과가 나온다.
이렇게 클래스에 위치한 값이 인스턴스의 동작에 영향을 주면 클래스의 추상성을 해치는 것이다. 인스턴스와의 관계에서는 구체적인 데이터를 지니지 않고 인스턴스가 사용할 메서드만을 지니는 틀로 작용하도록 구현해야한다.
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
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) {
Rectangle.call(this, width, width);
};
Square.prototype = new Rectangle();
var sq = new Square(5);
console.log(sq.getArea()); // 25
❗️ 문제 1. 이 코드도 클래스에 있는 값이나 인스턴스에 영향을 줄 수 있는 동일한 구조이다.
Rectangle내의 height, width에 새 값을 할당하고 Square의 width, height를 지우면 프로토타입 체이닝에 의해 이상한 결과가 나온다.
❗️ 문제 2. Square(.__proto__).constructor가 Rectangle을 가리키고 있다.
- 일단 상속 후 클래스 내부의 프로퍼티를 일일이 지우고 더는 새로운 프로퍼티를 추가할 수 없게 하는 방법
delete Square.prototype.width; delete Square.prototype.height; Object.freeze(delete Square.prototype);
var Rectangle = function (width, height) {
this.width = width;
this.height = height;
};
Rectangle.prototype.getArea = function () {
return this.width * this.height;
};
var extendClass1 = function (SuperClass, SubClass, subMethods) {
SubClass.prototype = new SuperClass();
for (var prop in SubClass.prototype) {
if (SubClass.prototype.hasOwnProperty(prop)) {
// 프로토타입들을 돌면서 프로퍼티만 삭제
delete SubClass.prototype[prop];
}
}
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);
});
var sq = new Square(5);
console.log(sq);
- 더글라스 크락포드가 제시한 방법
빈 함수를 이용한다는 특징이 있다.
var Rectangle = function (width, height) {
this.width = width;
this.height = height;
};
Rectangle.prototype.getArea = function () {
return this.width * this.height;
};
var Bridge = function () {};
var Square = function (width) {
Rectangle.call(this, width, width);
};
Bridge.prototype = Rectangle.prototype;
// Bridge의 프로토타입이 Rectangle의 프로토타입을 참조
Square.prototype = new Bridge();
// Square의 프로토타입에 Bridge의 인스턴스를 할당하면 Rectangle 자리에 Bridge가 대체한다.
// 이로써 프로토타입 체인 경로상에는 구체적인 데이터가 남아있지 않게 된다.
Object.freeze(Square.prototype);
var sq = new Square(5);
console.log(sq);
var Rectangle = function (width, height) {
this.width = width;
this.height = height;
};
Rectangle.prototype.getArea = function () {
return this.width * this.height;
};
var extendClass2 = (function () {
var Bridge = function () {};
return function (SuperClass, SubClass, subMethods) {
Bridge.prototype = SuperClass.prototype;
SubClass.prototype = new Bridge();
if (subMethods) {
for (var method in subMethods) {
SubClass.prototype[method] = subMethods[method];
}
}
Object.freeze(SubClass.prototype);
return SubClass;
};
})();
var Square = extendClass2(Rectangle, function (width) { Rectangle.call(this, width, width) });
var sq = new Square(5);
console.log(sq);
- ES5 Object.create 활용
Square.prototype = Object.create(Rectangle.prototype); Object.freeze(Square.prototype);
- 이 방식은 SubClass의 prototype의 __proto__가 SuperCalss의 prototype을 바라보되, SuperClass의 인스턴스가 되지는 않아 안전하다.
위 세가지 방법은 기본적인 상속은 성공했지만 SubClass의 constructor가 SuperClass를 가리키는 문제가 있다. 엄밀히는 SubClass 인스턴스는 constructor가 없고 SubClass.prototype에도 없는 상태이다. 체이닝 상 가장 먼저 등장하는 SuperClass.prototype.constructor가 출력되는 것이다.
1 번 방법
var Rectangle = function (width, height) {
this.width = width;
this.height = height;
};
Rectangle.prototype.getArea = function () {
return this.width * this.height;
};
var extendClass1 = function (SuperClass, SubClass, subMethods) {
SubClass.prototype = new SuperClass();
for (var prop in SubClass.prototype) {
if (SubClass.prototype.hasOwnProperty(prop)) {
// 프로토타입들을 돌면서 프로퍼티만 삭제
delete SubClass.prototype[prop];
}
}
SubClass.prototype.constructor = SubClass;
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);
});
var sq = new Square(5);
console.log(sq);
2번 방법
var Rectangle = function (width, height) {
this.width = width;
this.height = height;
};
Rectangle.prototype.getArea = function () {
return this.width * this.height;
};
var extendClass2 = (function () {
var Bridge = function () {};
return function (SuperClass, SubClass, subMethods) {
Bridge.prototype = SuperClass.prototype;
SubClass.prototype = new Bridge();
SubClass.prototype.constructor = SubClass;
if (subMethods) {
for (var method in subMethods) {
SubClass.prototype[method] = subMethods[method];
}
}
Object.freeze(SubClass.prototype);
return SubClass;
};
})();
var Square = extendClass2(Rectangle, function (width) { Rectangle.call(this, width, width) });
var sq = new Square(5);
console.log(sq);
3번 방법
Square.prototype = Object.create(Rectangle.prototype);
Square.prototype.constructor = Square;
Object.freeze(Square.prototype);
하위 클래스의 메서드에서 상위 크랠스의 메서드 실행 결과를 바탕으로 작업하고 싶을 때가 있다. 이럴 때 마다
SuperClass.prototype.method.apply(this, arguments)
로 접근하는 것은 번거롭다.
→ 다른 객체지향 언어들의 클래스 묹법인'super'
를 흉내 내보고자 한다.
var extendClass = function (SuperClass, SubClass, subMethods) {
SubClass.prototype = Object.create(SuperClass.prototype);
SubClass.prototype.constructor = SubClass;
SubClass.prototype.super = function (propName) { // 추가된 부분 시작
var self = this;
if(!propName) return function () {
// 비었을 경우 SuperClass 생성자 함수에 접근하는 것으로 간주
SuperClass.apply(self, arguments);
// this가 달라지는 것을 방지하기 위해 클로저 사용
}
var prop = SuperClass.prototype[propName];
if(typeof prop !== 'function') return prop; // propName에 해당하는 값이 함수가 아닌 경우 그 값을 반환
return function() {
return prop.apply(self, arguments); // 메서드에 접근
}
}; // 추가된 부분 끝
if (subMethods) {
for (var method in subMethods) {
SubClass.prototype[method] = subMethods[method];
}
}
Object.freeze(SubClass.prototype);
return SubClass;
};
var Square = extendClass (Rectangle, function(width){
this.super()(width,width); // super 사용 (1) SuperClass 생성자 함수
}, {
getArea: function(){
console.log('size is :', this.super('getArea')()); // super 사용 (2) => size is : 100
}
})
ES6에서 클래스 문법이 도입되었다.
var ES5 = function (name) {
this.name = name;
};
ES5.staticMethod = function () {
return this.name + " static method";
};
ES5.prototype.method = function () {
return this.name + " method";
};
var es5Instance = new ES5("es5");
console.log(ES5.staticMethod()); // 'ES5 static method'
console.log(es5Instance.method()); // 'es5 method'
var ES6 = class {
constructor(name) {
this.name = name;
}
static staticMethod() {
return this.name + " static method";
}
method() {
return this.name + " method";
}
};
var es6Instance = new ES6("es6");
console.log(ES6.staticMethod()); // 'ES6 static method'
console.log(es6Instance.method()); // 'es6 method'