본문 바로가기
JavaScript | 자바스크립트

JavaScript - class 다중상속

by Pig_CoLa 2020. 7. 29.
SMALL

다중상속이란

일반적인 상속(prototype chain)은 다음과 같이 구성되어있다.

  • A클래스
  • A를 상속받은 B클래스
  • B를 상속받은 C클래스
  • C를 상속받은 D클래스

 

하지만 간혹 특수한 상속이 필요할 때가 존재한다.

 

  • A클래스

    • A를 상속받은 B클래스

      • B를 상속받은 C클래스

  • D클래스

    • D를 상속받은 E클래스

      • E를 상속받은 F클래스

  • C클래스와 F클래스를 모두 상속받은 G클래스

 

JavaScript에서 이러한 유형의 상속은 불가능하다.

 

타언어였다면...?

타 언어들을 기준으로 다중상속이 가능한 언어들이 있다.

다만 해당 언어들도 모든것이 완벽한것이 아니다.

몇가지 규칙이 있는데

 

  1. 다중상속시 우선순위 되는 class가 있다.
  2. 상속받은 class에 없는 메서드 호출시 우선순위가 되는 class의 상속관계 라인에서 먼저 확인한다.
    (JavaScript로 치자면 prototype-chain을 확인하는 것이다. Python에선 이를 mro라고 한다.)
  3. 없을경우 후순위 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

이 방법은 삽질을 몇일간 하다가 여러 특징을 조합하여 찾아낸것 으로

해당 특징들에 대하여 먼저 기술한다.

  1. ES5문법으로 ES6문법의 class 상속하기
  2. 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문으로 받아올 수가 없다.

그렇기에 ObjectgetOwnPropertyNames메서드를 사용하여 열거가능하지 않은 속성이나 메서드의 이름을 받아오고, 그에대한 반복을 실행한다.

예제

// 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를 수정할 수가 없거나, 각 메서드 들을 조회할 수 없다면
또는 상속관계 확인이 필요없거나 다른방법으로 대체할 수 있다면 충분히 유용하게 쓰일 수 있어 보인다.

 

 

참고한 자료

열거속성

getOwnPropertyNames

 

LIST

'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

댓글