자바스크립트는 프로토타입 기반 언어라서 '상속' 개념이 존재하지 않는다.
클래스와 비슷하게 동작하게끔 흉내 내는 여러 기법들이 탄생했다.
결국 ES6에 클래스 문법이 추가되었지만, ES6 클래스에서도 일정 부분은 프로토타입을 활용하고 있기 때문에,
ES5 체제 하에서 클래스를 흉내내기 위한 구현 방식을 학습하는 것이 큰 의미가 있다.
객체지향 프로그래밍에서의 클래스 계급 집단 집합
어떤 가게에 다양한 음식이 한 개씩 있다고 했을 때,
음식 -> 고기 채소 과일
과일 -> 배 사과 바나나 감 오렌지
배 사과 바나나는 직접 만질 수 있고 볼 수 있는 구체적이고 실존하는 사물
음식 과일은 어떤 사물들의 공통 속성을 모아 정의한 것으로 직접 만질 수도 볼 수도 없는 추상적인 개념
음식은 과일과의 관계에서 상위 개념(superclass)이고,
과일은 음식과의 관계에서 하위 개념(subclass)이다.

과일 분류 하위에 또 다른 분류가 있을 경우 클래스 간의 관계는 어떻게 될까?

음식은 과일의 상위 개념(superclass)이며 귤류의 최상위 개념(super-superclass)이다.
과일은 음식의 하위 개념(subclass)이다.
귤류는 음식의 최하위 개념(sub-subclass)이다.
클래스는 하위로 갈수록 상위 클래스의 속성을 상속하면서 더 구체적인 요건이 추가 또는 변경된다.
물론 하위 클래스가 아무리 구체화되더라도 이들은 결국 추상적인 개념이다.
감귤 자몽 천혜향 등은 먹을 수 있고, 과일 및 귤류에 속하는 구체적인 개체들이다.
어떤 클래스의 속성을 지니는 실존하는 개체를 일컬어 인스턴스(instance)라 한다.
인스턴스 === 어떤 조건(클래스)에 부합하는 구체적인 예시
컴퓨터는 사용자가 직접 여러 가지 클래스를 정의해야 하며,
클래스를 바탕으로 인스턴스를 만들 때 비로소 어떤 개체가 클래스의 속성을 지니게 된다.
또한 한 인스턴스는 하나의 클래스만을 바탕으로 만들어진다.
어떤 인스턴스가 다양한 클래스에 속할 수는 있지만, 이 클래스들은 모두 인스턴스 입장에서는 직계존속이다.
다중상속을 지원하는 언어이든 그렇지 않은 언어이든 결국 인스턴스를 생성할 때 호출할 수 있는 클래스는 오직 하나뿐일 수밖에 없기 때문이다.
클래스는 공통 요소를 지니는 집단을 분류하기 위한 개념
클래스가 먼저 정의돼야만 그로부터 공통적인 요소를 지니는 개체들을 생성할 수 있다.
사용하기에 따라 추상적인 대상일 수도 있고 구체적인 개체가 될 수도 있다.
자바스크립트는 프로토타입 기반 언어이므로 클래스의 개념이 존재하지 않지만, 비슷한 요소는 있다.
생성자 함수Array를 new 연산자와 함께 호출하면 인스턴스가 생성된다.
이때 Array를 일종의 클래스라고 하면,
Array의 prototype 객체 내부 요소들이 인스턴스에 '상속'된다고 볼 수 있다.
엄밀히는 상속이 아닌 프로토타입 체이닝에 의한 참조지만 결과적으로는 동일하게 동작한다.
한편 Array 내부 프로퍼티들 중 prototype 프로퍼티를 제외한 나머지는 인스턴스에 상속되지 않는다.
인스턴스에 상속되는지(인스턴스가 참조하는지) 여부에 따라
스태틱 멤버 static member와 인스턴스 멤버 instance member로 나뉜다.
(클래스 입장에서 사용 대상에 따라 구분)
클래스 기반 언어와 달리 자바스크립트에서는 인스턴스에서도 직접 메서드를 정의할 수 있기 때문에
인스턴스 메서드라는 명칭은 프로토타입에 정의한 메서드를 지칭하는 것인지 인스턴스에 정의한 메서드를 지칭하는 것인지 헷갈리기 때문에 프로토타입 메서드라고 부른다.

생성자 함수와 인스턴스
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 instace instaneof Rectangle &&
instance.width > 0 && instance.height > 0;
};
// Reactangle 함수를 new 연산자와 함께 호출해서 생성된 인스턴스를 rect1에 할당
var rect1 = new Rectangle(3, 4)
// 프로토타입 메서드: 인스턴스에서 직접 호출할 수 있는 메서드
// 프로토타입 객체에 할당한 메서드는 인스턴스가 마치 자신의 것처럼 호출할 수 있음
// rect1.__proto__.getArea에 접근
// __proto__를 생략했으니 thisrk rect1인 채로 실행됨
// => rect1.width * rect1.height의 계산값 반환
console.log(rect1.getArea()); // 12 (0)
// 스태틱 메서드: 인스턴스에서 직접 접근할 수 없는 메서드
// rect1 인스턴스에서 isRectangle이라는 메서드에 접근
// 해당 메서드는 rect1, rect1.__proto__, rect1.__proto__.__proto__(= Object.prototype)에도 없음
// -> undefined를 실행하라는 명령
// => 함수가 아니여서 실행할 수 없다는 "Uncaught TypeError: not a function" 에러 발생
console.log(rect1.isRectangle(rect1)); // Error (X)
// 스태틱 메서드는 생성자 함수를 this로 해야만 호출할 수 있음
console.log(Rectangle.isRectangle(rect1)); // true

프로그래밍 언어의 클래스는 사용하기에 따라 추상적일수도 있고 구체적인 개체가 될 수도 있다.
구체적인 인스턴스가 사용할 메서드를 정의한 '틀'의 역할을 담당하는 목적을 가질 때의 클래스는 추상적인 개념이지만,
클래스 자체를 this로 해서 직접 접근해야만 하는 스태틱 메서드를 호출할 때의 클래스는 그 자체가 하나의 개체로서 취급된다.
위 코드에서 '추상적인 개념'은 Rectangle 클래스 자체, 즉 '사각형'을 표현하는 이 틀이 된다.
isRectangle 메소드가 '어떤 객체가 유효한 사각형인지 판별하는 방법'을 추상화하고 있으므로, 일종의 '추상적인 개념'으로 볼 수 있다.
클래스 상속은 객체지향에서 가장 중요한 요소 중 하나이다.
ES5까지의 자바스크립트 커뮤니티에서는 클래스 상속을 다른 객체지향 언어에 익숙한 개발자들에게 최대한 친숙한 형태로 흉내 내는 것이 주요한 관심사였다.
프로토타입 체인을 활용해 클래스 상속을 구현하고 최대한 전통적인 객체지향 언어에서의 클래스와 비슷한 형태로까지 발전시켜보며 이해해보자 ~
Array 내장 클래스를 상속하는 Grade 클래스
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);
ES5 자바스크립트에서 클래스 상속을 구현했다는 것은 프로토타입 체이닝을 잘 연결한 것이다.
세부적으로 완벽하게 superclass와 subclass의 구현이 이뤄진 것은 아니다.
위 코드의 문제점

...
g.push(90);
console.log(g); // Grade { 0: 100, 1: 80, 2: 90, length: 3 }
delete g.length;
g.push(70);
// push한 값이 0번째 인덱스에 들어갔고
// length가 1이 됐다
console.log(g); // Grade { 0: 70, 1: 80, 2: 90, length: 1 }
내장객체인 배열 인스턴스의 length 프로퍼티는 configurable 속성이 false라서 삭제가 불가능하지만,
Grade 클래스의 인스턴스는 배열 메서드를 상속하지만 기본적으로는 일반 객체의 성질을 그대로 지니므로 삭제가 가능해서 문제가 된다.
push 했을 때 0번째 인덱스에 70이 들어가고 length가 다시 1이 될 수 있었던 이유는?
g.__proto__ 즉, Grade.prototype이 빈 배열을 가리키고 있기 때문이다.
push 명령에 의해 자바스크립트 엔진이 g.length를 읽고자 하는데 g.length가 없으니까
프로토타입 체이닝을 타고 g.__proto__.length를 읽어온 것이다.
빈 배열의 length가 0이므로 여기에 값을 할당하고 length는 1만큼 증가시키라는 명령이 문제 없이 동작할 수 있었던 것이다.
그럼 만약 Grade.prototype에 요소를 포함하는 배열을 매칭시켰다면 어땠을까?
...
Grade.prototype = ['a', 'b', 'c', 'd'];
var 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);
// g.length가 없으니까 g.__proto__.length를 찾고,
// 값이 4이므로 인덱스 4에 70을 넣고,
// 다시 g.length에 5를 부여하는 순서로 동작
console.log(g); // Grade { 0: 100, 1: 80, 2: 90, ____ 4: 70, length: 5 }
이처럼 클래스에 있는 값이 인스턴스의 동작에 영향을 줘서는 안된다.
영향을 줄 수 있다는 사실 자체가 클래스의 추상성을 해치는 것이다.
인스턴스와의 관계에서는 구체적인 데이터를 지니지 않고
오직 인스턴스가 사용할 메서드만을 지니는 추상적인 '틀'로서만 작용하게끔 작성하지 않는다면,
언젠가 어딘가에서 위 코드들과 같이 예기치 않은 오류를 발생할 가능성을 안고 가야한다.
사용자가 정의한 두 클래스 사이에서의 상속관계를 구현해보자
직사각형 클래스와 정사각형 클래스에 getArea라는 메서드 추가
var Reactangle = 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
// Square의 생성자 함수 내부에서 Rectangle의 생성자 함수를 함수로써 호출
// 인자 height 자리에 width를 전달
var Square = function (width) {
Rectangle.call(this, width, width)
};
// 메서드를 상속하기 위해 Square의 프로토타입 객체에 Rectangle의 인스턴스를 부여
Square.prototype.getArea = new Rectangle();
var sq = new Square(5);
console.log(sq.getArea()); // 25
정사각형은 직사각형에 '네 변의 길이가 모두 같다'라는 구체적인 조건이 추가된 개념이다.
Square를 Rectangle의 하위 클래스로 삼을 수 있다.
getArea라는 메서드는 동일한 동작을 하므로 상위 클래스에서만 정의하고,
하위 클래스에서는 해당 메서드를 상속하면서 height 대신 width를 넣어주면 된다.
완벽한 클래스 체계가 구축된 것은 아니다.
클래스에 있는 값이 인스턴스에 영향을 줄 수 있는 구조라는 동일한 문제를 가진다.
console.dir(sq);

sq의 구조
Square의 인스턴스width, height 5__proto__는 Rectangle의 인스턴스임을 표시하고 있는데,width, height에 모두 undefined가 할당돼 있음을 확인할 수 있다.문제 1. Square.prototype에 값이 존재
만약 이후에 임의로 Square.prototype.width(또는 height)에 값을 부여하고 sq.width(또는 height)의 값을 지워버린다면 프로토타입 체이닝에 의해 엉뚱한 결과가 나오게 될 것이다.
문제 2. constructor가 여전히 Rectangle을 바라보고 있다.
sq.constructor로 접근하면 프로토타입 체이닝을 따라 sq.__proto__.__proto__,
즉 Rectangle.prototype에서 찾게 되며, 이는 Rectangle을 가리키고 있기 때문이다.
var rect2 = new sq.constructor(2, 3);
console.log(rect2); // Rectangle { width: 2, height: 3 }
이처럼 하위 클래스로 삼을 생성자 함수의 prototype에 상위 클래스의 인스턴스를 부여하는 것만으로도 기본적인 메서드 상속은 가능하지만 다양한 문제가 발생할 여지가 있어 구조적으로 안전성이 떨어진다.

클래스(prototype)가 구체적인 데이터를 지니지 않게 하는 가장 쉬운 방법은
일단 만들고 나서 프로퍼티들을 일일이 지우고 더는 새로운 프로퍼티를 추가할 수 없게 한다.
delete Square.prototype.width;
delete Square.prototype.height;
Object.freeze.(Square.prototype);
프로퍼티가 많다면 반복을 없애고 범용적인 함수를 만들자.
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);
});
extendClass1 함수는 SuperClass와 SubClass, SubClass에 추가할 메서드들이
정의된 객체를 받아서 SubClass의 prototype 내용을 정리하고 freeze한다.
SubClass의 프로토타입을 정리하는 내용이 복잡해졌지만 범용성 측면에서는 꽤 괜찮은 방법이다.
두 번째 방법은
SubClass의 prototype에 직접 SuperClass의 인스턴스를 할당하는 대신
아무런 프로퍼티를 생성하지 않는 빈 생성자 함수(Bridge)를 하나 더 만들어서
그 prototype이 SuperClass의 prototype을 바라보게끔 한 다음,
SubClass의 prototype에는 Bridge의 인스턴스를 할당하게 하는 것이다.
빈 함수에 다리 역할을 부여하는 것이다.
var Rectangle = function (width, height) {
this.width = width;
this.height = height;
};
Rectangle.prototypre.getArea = function () {
return this.width * this.height;
};
var Square = function (width) {
Rectangle.call(this, width, width);
};
var 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가 대체하게 될 것이다.
이로써 인스턴스를 제외한 프로토타입 체인 경로상에는 더는 구체적인 데이터가 남아있지 않게 된다.
마찬가지로 범용성을 고려하면 다음처럼 작성할 수 있다.
var extendClass2 = (function () {
var Bridge = function () {};
return functioin (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;
};
})();
즉시실행함수 내부에서 Bridge를 선언해서 이를 클로저로 활용함으로써
메모리에 불필요한 함수 선언을 줄였다.
subMethods에는 SubClass의 prototype에 담길 메서드들을 객체로 전달하게끔 한다.
마지막 방법으로 ES5에서 도입된 Object.create를 이용한 방법이다.
SubClass의 prototype의 __proto__가 SuperClass의 prototype을 바라보되,
SuperClass의 인스턴스가 되지는 않으므로 앞서 소개한 두 방법보다 간단하면서 안전하다.
// (...생략)
Square.prototype = Object.create(Rectangle.prototype);
Object.freeze(Square.prototype);
// (...생략)
클래스 상속 및 추상화를 흉내내기 위한 라이브러리가 많지만 기본 접근 방법은 이 아이디어다.
SubClass.prototype의 __proto__가 SuperClass.prototype를 참조하고,
SubClass.prototype에는 불필요한 인스턴스 프로퍼티가 남아있지 않으면 된다.
위 세 방법 모두 기본적인 상속에는 성공했지만
SubClass 인스턴스의 consturctor는 여전히 SuperClass를 가리키는 상태이다.
엄밀히는 SubClass 인스턴스에는 constructor가 없고, SubClass.prototype에도 없는 상태이다.
프로토타입 체인상에 가장 먼저 등장하는 SuperClass.prototype의 constructor에서 가리키는 대상,
즉 SuperClass가 출력될 뿐이다.
따라서 위 코드들의 SubClass.prototype.constructor가 원래의 SubClass를 바라보도록 해주면 된다.
var extendClass1 = function (SuperClass, SubClass, subMethods) {
SubCalss.prototype = new SuperClass();
for (var prop in SubCalss.prototype) {
if (SubClass.prototype.hasOwnProperty(prop)) {
delete SubClass.prototype[prop];
}
}
SubCalss.prototype.constructor = SubClass; //
if (subMethods) {
for (var method in subMethods) {
SubClass.prototype[method] = subMethods[method];
}
}
Object.freeze(SubClass.prototype);
return SubClass;
};
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 extendClass3 = function (SuperClass, SubClass, subMethods) {
SubClass.prototype = Object.create(SuperClass.prototype);
SubClass.prototype.constructor = SubClass;
if (subMethods) {
for (var method in subMethods) {
SubClass.prototype[method] = subMethods[method];
}
}
Object.freeze(SubClass.prototype);
return SubClass;
하위 클래스의 메서드에서 상위 클래스의 메서드 실행 결과를 바탕으로 추가적인 작업을 수행하고 싶을 때
매번 SuperClass.prototype.method.apply(this, arguments)로 해결하는 것은 상당히 번거롭고 가독성이 떨어지는 방식이다.
하위 클래스에서 상위 클래스의 프로토타입 메서드에 접근하기 위해 super를 흉내내보자.
var extendClass = function (SuperClass, SubClass, subMethods) {
SubClass.prototype = Object.create(SuperClass.prototype);
SubClass.prototype.constructor = SubClass;
// super 메서드의 동작 정의
SubClass.prototype.super = functioin (propName) { // 추가된 부분 시작
var self = this;
// 인자가 비어있을 경우 SuperClass 생성자 함수에 접근하도록
// this가 달라지는 것을 막기 위해 클로저 활용
if (!propName) return function () {
SuperClass.apply(self, arguments);
}
var prop = SuperClass.prototype[propName];
// SuperClass의 prototype 내부의 propName에 해당하는 값이 함수가 아닌 경우
// 해당 값을 그대로 반환
if (typeof prop !== 'function') return prop;
// 함수인 경우이므로
// 클로저를 활용해 메서드에 접근하도록
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 Rectangle = function (width, height) {
this.width = width;
this.height = height;
};
Rectangle.prototype.getArea = function () {
return this.width * this.height;
};
var Square = extendClass(
Rectangle,
function (width) {
// 기존 코드 SuperClass.call(this, width, width) 직접 this를 바인딩
this.super()(width, width); // super 사용 (1)
}, {
// SuperClass의 getArea 메서드 확장
getArea: function () {
console.log('size is :', this.super('getArea')()); // super 사용 (2)
}
}
};
var sq = new Square(10);
// SubClass의 메서드 실행
sq.getArea(); // size is: 100
// SuperClass의 메서드 실행
console.log(sq.super('getArea')()); // 100
ES6에서 클래스 문법 도입
var ES5 = function (name) {
this.name = name;
};
ES5.staticMethod = function () {
return this.name + ' staticMethod';
};
ES5.prototype.method = function () {
return this.name + ' method';
};
var es5Instance = new ES5('es5');
console.log(ES5.staticMethod)()); // es5 staticMethod
console.log(es5Instance.method()); // es5 method
var ES6 = calss {
// 생성자 함수와 동일한 역할
constructor (name) { // 클래스 본문에서 function 생략해도 모두 메서드로 인식
this.name = name;
} // 메서드와 메서드 사이에는 콤마,로 구분하지 않음
// 생성자 함수에 바로 할당하는 메서드와 동일하게 생성자 함수(클래스) 자신만이 호출 가능
static staticMethod () { // 해당 메서드가 static 메서드임을 알림
return this.name + ' staticMethod';
}
// 자동으로 prototype 객체 내부에 할당되는 메서드
// ES5.prototype.method와 동일하게, 인스턴스가 프로토타입 체이닝을 통해
// 마치 자신의 것처럼 호출할 수 있는 메서드
method () {
return this.name + ' method';
}
};
var es6Instance = new ES6('es6');
console.log(ES6.staticMethod()); // es6 staticMethod
console.log(es6Instance.method()); // es6 method
var Rectangle = class {
constructor (width, height) {
this.width = width;
thid.height = height;
}
getArea () {
return this.width * this.height;
}
};
// Square를 Rectangle 클래스를 상속받는 SubClass로 만들기 위해
// class 명령어 뒤에 extends Rectangle 추가하여 상속 관계 설정함
var Square = class extends Rectangle {
constructor (width) {
// constructor 내부에서 super라는 키워드를 함수처럼 사용가능
// 함수는 SuperClass의 constructor를 실행
super(width, width);
}
getArea() {
// constructor 메서드를 제외한 다른 메서드에서는 super 키워드를 객체처럼 사용할 수 있고
// 이때 객체는 SuperClass.prototype을 바라보는데,
// 호출한 메서드의 this는 'super'가 아닌 원래 this를 그대로 따른다
console.log('size is :', super.getArea());
}
};
자바스크립트는 프로토타입 기반 언어라서 클래스 및 상속 개념은 없지만
프로토타입을 기반으로 클래스와 비슷하게 동작하게끔 하는 다양한 기법이 도입되어왔다.
클래스: 어떤 사물의 공통 속성을 모아 정의한 추상적인 개념
인스턴스: 클래스의 속성을 지니는 구체적인 사례
상위 클래스 superclass의 조건을 충족하면서
더욱 구체적인 조건이 추가된 것을 하위 클래스 subclass라고 한다.
클래스의 prototype 내부에 정의된 메서드를 프로토타입 메서드라고 하며,
이들은 인스턴스가 마치 자신의 것처럼 호출할 수 있다.
스태틱 메서드: 클래스(생성자 함수)에 직접 정의한 메서드
인스턴스가 직접 호출할 수 없고 클래스(생성자 함수)에 의해서만 호출할 수 있다.
클래스 상속 흉내내는 방법
모두 constructor 프로퍼티가 원래의 생성자 함수를 바라보도록 조정해야 한다.
super: 상위 클래스에 접근할 수 있는 수단
=> ES6에서 클래스 생김