다중상속이란
일반적인 상속(prototype chain)은 다음과 같이 구성되어있다.
- A클래스
- A를 상속받은 B클래스
- B를 상속받은 C클래스
- C를 상속받은 D클래스
하지만 간혹 특수한 상속이 필요할 때가 존재한다.
-
A클래스
-
A를 상속받은 B클래스
-
B를 상속받은 C클래스
-
-
-
D클래스
-
D를 상속받은 E클래스
-
E를 상속받은 F클래스
-
-
-
C클래스와 F클래스를 모두 상속받은 G클래스
JavaScript에서 이러한 유형의 상속은 불가능하다.
타언어였다면...?
타 언어들을 기준으로 다중상속이 가능한 언어들이 있다.
다만 해당 언어들도 모든것이 완벽한것이 아니다.
몇가지 규칙이 있는데
- 다중상속시 우선순위 되는 class가 있다.
- 상속받은 class에 없는 메서드 호출시 우선순위가 되는 class의 상속관계 라인에서 먼저 확인한다.
(JavaScript로 치자면 prototype-chain을 확인하는 것이다. Python에선 이를 mro라고 한다.) - 없을경우 후순위 class의 mro를 확인한다.
JavaScript에서 다중상속 구현하기
언어마다 특징이 있기때문에 타 언어를 참조(비교)하는것은 어리석은 일이 될 가능성이 높지만
현재 언어에서 제공되지 않는 특징을 구현하기 위해서는 타 언어를 참조하는것이 필수적으로 동반된다.
위에 기술한 타 언어에서의 다중상속 특징을 참고하여 JavaScript에서 구현해보자.
방법을 기술하기에 앞서 지극히 주관적인 내용임을 밝히며, Project가 커질수록 상속관계여부를 체크하는것이 중요해지기 때문에 이 방법으로는 한계점이 올수 있음을 유의할 것. 언어자체에서 지원하지 않아 여러 부작용이 있음을 되새긴 후 사용할 것.
방법1
class를 선언할때 표현식도 가능하다는 것을 활용한다.
각 class를 선언할때 사용해야 하는 방법이다.
// 방법 1 //
Person = (base) => class Person extends (base || Object) {
constructor(name, universityName) {
({ name, universityName } = (typeof name === 'object' && !Array.isArray(name) && arguments.length === 1) ? name : { name, universityName });
super({ name, universityName });
this.id = Math.floor(Math.random() * (10000 - 1 + 1)) + 1;
this.name = name;
}
hi() {
console.log(`my name is ${this.name}`);
}
}
University = (base) => class University extends (base || Object) {
constructor(universityName = {}) {
let { universityName: name } = (typeof universityName === 'object' && !Array.isArray(universityName)) ? universityName : { universityName };
super();
this.UniversityName = name;
}
}
class 대학생 extends Person(University()) {
constructor(name, universityName) {
super({ name, universityName });
this.studentID = `${this.UniversityName}-${this.id}`;
delete this.id;
}
}
let a = new 대학생('황대성', '대성대학교');
console.log(a);
// 대학생 { UniversityName: "대성대학교", name: "황대성", studentID: "대성대학교-3164" }
// UniversityName: "대성대학교"
// name: "황대성"
// studentID: "대성대학교-3164"
// __proto__: Person
// constructor: class 대학생
// __proto__: University
// constructor: class Person
// hi: ƒ hi()
// __proto__: Object
이와같이 표현식이 평가되기 전에는 class가 존재하지 않는것을 활용하여 나타낼 수 있지만
상속의 Level이(깊이가) 적어도 한쪽은 1이어야 하는 제약조건이 있으며
이는 곧 상속의 Level이 1인 class는 Object class를 상속해야 한다는 뜻이 된다.
(아무것도 상속받지 않는 경우가 Object class를 상속하는 경우이다)
그 경우가 아니라면, 처음부터 이와 같은 방법으로 class를 구성하여야 한다.
단점
단점은 특징 그 자체에 있다.
상속하려는 두개의 class중 하나는 무조건 상속의 깊이가 1이어야한다...
를 만족하는 상속패턴을 실사용에서는 찾아보기 힘들다.
방법 2
이 방법은 삽질을 몇일간 하다가 여러 특징을 조합하여 찾아낸것 으로
해당 특징들에 대하여 먼저 기술한다.
- ES5문법으로 ES6문법의 class 상속하기
- prototype 병합하기
1. ES5문법으로 ES6문법의 class 상속하기
ES6문법 으로 선언한 class는 class
키워드를 사용하기 때문에 new
키워드 없이 호출될 수 없다.
이에 따라서 ES5문법의 class상속이 불가능하다.(call
, apply
를 통한 호출이 불가능하다.)
대신 new키워드를 이용해 instance를 만들어주고
그 instance 속성들을 this에 대한 속성으로 만들어주면 가능하다.
예제
class A {
constructor(name) {
this.name = name;
}
hi() {
return `hi~ my name is ${this.name}`;
}
}
function B(name, age) {
let temp = new A(name);
for (let i in temp) {
this[i] = temp[i];
}
this.age = age;
}
B.prototype = Object.create(A.prototype);
B.prototype.constructor = B;
let a = new B("Hwang", 26);
console.log(a) // B {name: "Hwang", age: 26}
a.hi() // "hi~ my name is Hwang"
2. prototype 병합하기
SuperClass와 OtherSuperClass로 나누어준다.
이때 SuperClass의 우선순위가 더 높은것으로 해준다.
역시 두 class에 대한 prototype을 Object.create함수에 각각 넣어주고
OtherSuperClass에 대한 결과값을 순회하면서
SuperClass에 대한 결과값에 존재하지 않다면 넣어주면 된다.
이를 재귀적으로 OtherSuperClass에 대한 prototype-chain을 들어가며
constructor가 Object가 나올때까지 재귀호출한다.
하지만prototype
또는 __proto__
내부의 키들은 열거가능한 속성이 아니기 때문에
for ...in문으로 받아올 수가 없다.
그렇기에 Object
의 getOwnPropertyNames
메서드를 사용하여 열거가능하지 않은 속성이나 메서드의 이름을 받아오고, 그에대한 반복을 실행한다.
예제
// prototype을 병합하는 함수이다.
mixin = function (SuperClass, OtherSuperClass) {
let result = Object.create(SuperClass.prototype);
let temp = Object.create(OtherSuperClass.prototype);
if (temp.__proto__.constructor !== Object) {
recur(result, temp.__proto__)
}
return result;
function recur(A, B) {
let temp = Object.getOwnPropertyNames(B);
for (let i of temp) {
if (!(i in A)) {
A[i] = B[i];
}
}
if (B.__proto__.constructor !== Object) {
recur(A, B.__proto__)
}
}
}
class A {
constructor(name) {
this.name = name;
}
hi() {
return `my name is ${this.name}`;
}
}
class AA extends A {
constructor(name, age) {
super(name);
this.age = age;
}
}
class B {
constructor(hobby) {
this.hobby = hobby;
}
}
class BB extends B {
constructor(hobby, from) {
super(hobby);
this.from = from;
}
whereFrom() {
return `i'm from ${this.from}.`;
}
}
console.log(mixin(AA, BB))
// AA {whereFrom: ƒ}
// whereFrom: ƒ whereFrom()
// __proto__: A
// constructor: class AA
// __proto__:
// constructor: class A
// hi: ƒ hi()
// __proto__: Object
이 방법을 사용하여 prototype을 병합고 이를 상속받을 곳에 할당해 준다면
다중상속을 받은 class의 method인지 물려받은 것인지 파악되지 않는다.
그렇기기에 이 방법으로 다중상속을 진행할경우 ES5문법으로 다중상속후
이에대하여 ES6문법으로 다시 상속받고 속성과 메서드를 정의하면 된다.
다중상속 예시
A클래스를 상속받은 AA클래스와, B클래스를 상속받은 BB클래스 두개를
동시에 상속받는 CC클래스 생성하기
mixin = function (SuperClass, OtherSuperClass) {
let result = Object.create(SuperClass.prototype);
let temp = Object.create(OtherSuperClass.prototype);
if (temp.__proto__.constructor !== Object) {
recur(result, temp.__proto__)
}
return result;
function recur(A, B) {
let temp = Object.getOwnPropertyNames(B);
for (let i of temp) {
if (!(i in A)) {
A[i] = B[i];
}
}
if (B.__proto__.constructor !== Object) {
recur(A, B.__proto__)
}
}
}
class A {
constructor(name) {
this.name = name;
}
hi() {
return `my name is ${this.name}`;
}
}
class AA extends A {
constructor(name, age) {
super(name);
this.age = age;
}
}
class B {
constructor(hobby) {
this.hobby = hobby;
}
}
class BB extends B {
constructor(hobby, from) {
super(hobby);
this.from = from;
}
whereFrom() {
return `i'm from ${this.from}.`;
}
}
// 다중상속 시작
// CC를 만들기 위한 초석
function MixC(name, age, hobby, from) {
let temp1 = new AA(name, age);
let temp2 = new BB(hobby, from);
for (let i in temp2) {
this[i] = temp2[i];
}
for (let i in temp1) {
this[i] = temp1[i];
}
}
MixC.prototype = mixin(AA, BB);
MixC.prototype.constructor = MixC;
// 만들어둔 초석을 상속: 이렇게 하면 메서드를 정의하더라도
// mixin된 prototype에 들어가는 것이 아니기 때문에 mixin과정에서 추가된 메서드와
// 다중상속을 받은 클래스에서 추가된 메서드를 구별할 수 있다.
class CC extends MixC {
constructor(name, age, hobby, from) {
super(name, age, hobby, from);
this.state = '개피곤'
}
sleep() {
return '잠은 무슨 공부나 더해';
}
}
// CC의 instance 생성
let a = new CC("황대성", 26, "없음", "전주")
console.log(a);
// CC {hobby: "없음", from: "전주", name: "황대성", age: 26, state: "개피곤"}
// A클래스의 메서드 hi
a.hi(); // "my name is 황대성"
// BB클래스의 메서드 whereFrom
a.whereFrom(); // "i'm from 전주."
// CC클래스의 메서드 sleep
a.sleep(); // "잠은 무슨 공부나 더해"
단점
instance가 CC, MixC, AA, A 각각의 class에 대하여 instanceof 값이 true가 나오지만
BB와 B는 false가 된다. 즉 prototype chain내에서 검사하는듯 하다.
이미 상단에 기재했지만, 이러한 (instanceof를 통한 class소속 확인) 작업을 우선순위로 필요하다면
처음부터 갈아엎고 다시 작성하는것이 더 도움이 될 수도 있다.
하지만 class
를 수정할 수가 없거나, 각 메서드 들을 조회할 수 없다면
또는 상속관계 확인이 필요없거나 다른방법으로 대체할 수 있다면 충분히 유용하게 쓰일 수 있어 보인다.
참고한 자료
'JavaScript | 자바스크립트 > 기타' 카테고리의 다른 글
Promise / async / await (0) | 2020.08.17 |
---|---|
OOP - 객체지향 / 상속 (0) | 2020.07.29 |
복잡도 - 시간복잡도 (0) | 2020.07.19 |
재귀호출 (0) | 2020.07.18 |
함수의 메서드 - call, apply, bind (2) | 2020.07.17 |
댓글