Today What I Learned

Javascript를 배우고 있습니다. 매일 배운 것을 이해한만큼 정리해봅니다.


상속을 활용한 Subclassing

  • 객체지향언어에서 상속이란 하위 객체가 상위 객체의 (자바스크립트에서는 프로토타입으로 체이닝 되어) 속성과 메소드를 활용하게 되는 것을 가리킨다.
  • 객체지향언어에서는 상속을 통해 현실 세계에 있는 다양한 경우들을 구현하고자 노력한다.
  • 참고: https://poiemaweb.com/js-prototype
  • 자바스크립트에서는 객체 생성 시 internal slot [[prototype]]이 생긴다. [[prototype]]은 __proto__(dunder proto)라는 객체의 속성을 통해 접근할 수 있는데, 부모 객체의 prototype 값을 바라보면서 객체 간 상속을 가능하게 하고 있다.
  • 자바스크립트는 객체 지향을 목적으로 만들어진 언어가 아니었지만, 계속해서 객체 지향을 구현하고자 다양한 방법이 시도 되었고 ES5에서는 Pseudoclassical subclassing이, ES6에서는 Class 문법을 이용한 subclassing이 가능한 언어이다(class-free language).

1. ES5 Pseudoclassical Subclassing

  • 인스턴스를 만들 원형 객체를 만들어 속성을 키:밸류 값으로 넣는다. 이 때 상위 객체로부터 상속 받을 속성이 있다면 자체 속성 정의 전, 상위 객체.call(this)의 형태(변수가 여럿이면 상위 객체.apply(this, arguments)도 가능)로 속성을 불러와 1)상속하여 그대로 사용하거나 2)하위 객체에서 새로운 속성 값으로 할당할 수 있다.

  • 객체 선언 후에는 객체의 prototype에 정의하고자 하는 메소드 함수들을 정의하는데, 이 때도 상위 객체로부터 상속 받기 위해 객체의 __proto__을 통해 상위 객체의 prototype을 바라볼 수 있도록 객체.prototype = Object.create(상위객체.prototype) 의 형태로 선언한다.

  • 이로써 객체의 prototype은 상위 객체의 prototype을 바라보게 되었다. 그러나 아직 객체 일 뿐 생성자 함수처럼 constructor를 가지지 못했기 때문에 완벽한 형태는 아니다.

  • 이를 해결하기 위해 객체.prototype.constructor = 객체 자신 을 선언하여 인스턴스를 생성할 수 있도록 해준다.

  • 그런 다음 상속 받은 메소드 함수를 사용하거나 새로운 함수를 선언한다. 이 때, 상속 받은 함수와 같은 이름을 재정의 하고 사용하면 prototype chaining을 하지 않고 객체 자신이 가지고 있는 함수를 실행한다. 따라서 이 과정에서 상속의 관계를 잘 생각해야 한다.

  • 말로만 하면 너무 어렵기 때문에 오늘 스프린트로 진행했던 유충-벌-꿀벌의 상속 관계를 예시로 한 번 들어 보겠다.

    // 1. Grub이라는 객체를 선언하고 속성값과 가지고 있을 함수를 할당한다.
    var Grub = function() {
      this.age = 0;
      this.color = 'pink';
      this.food = 'jelly';
    }
    Grub.prototype.eat = function() {
      return 'ate';
    }

    //2. Grub으로 생성된 객체는 어떤 모양인지 확인해본다.
    var grub = Grub();
    console.log(grub.age); // 0
    console.log(grub.color); // pink
    console.log(grub.food); // jelly
    console.log(grub.eat()); // ate

    //3. Grub을 상속 받은 Bee라는 객체를 만들어본다.
    var Bee = function() {
      Grub.call(this); //이 과정에서 Grub의 prototype 안에 속성을 참조할 수 있게 된다.
      this.age = 5; // 상속 받은 속성값을 재할당 할 수도 있다.
      this.color = 'yellow';
      this.job = 'keep on growing'; // Bee만 가지는 새로운 속성값도 할당 가능
    }

    //4. 상속 받은 함수 사용을 위해 prototype을 연결한다.
    Bee.prototype = Object.create(Grub.prototype);

    //5. 생성자 함수 역할을 할 수 있도록 constructor를 자신으로 지정한다.
    Bee.prototype.constructor = Bee; // 생성자 함수의 prototype의 constructor는 자기 자신

    //6. Grub에서 상속 받은 함수를 재정의 하거나 Bee만 가지는 자신만의 함수를 선언한다.
    Bee.prototype.makeSound = function() {
      return 'weeeing';
    } // 상속 받은 것 외에 새로운 함수 정의

    //7. Bee의 상속이 제대로 이루어졌는지 확인해본다.
    var bee = Bee();
    console.log(bee.age); // 5
    console.log(bee.color); // yellow
    console.log(bee.eat()); // ate - 그대로 상속
    console.log(bee.job); // keep on growing
    console.log(bee.eat()); // ate - 그대로 상속
    console.log(bee.makeSound()); // weeeing - 그대로 상속
  • 유충인 Grub의 속성과 함수가 조금 성장한 벌 Bee에 상속된 경우를 살펴 보았다.

  • 이번에는 한 단계 더 나아가 Bee를 상속한 꿀벌 HoneyBee를 살펴 보겠다.

    //1. HoneyBee라는 객체를 생성하며 Bee의 속성값을 연결한다.
    var HoneyBee = function() {
      Bee.call(this, age, job); // 이번에는 Bee라는 실행 문맥과 age, job 속성값만 가져옴
      this.age = 10;
      this.job = 'make honey';
      this.honeyPot = 0; // HoneyBee만이 가진 속성값을 정의
    }

    //2. Bee와 동일하게 prototype과 constructor를 정의한다.
    var HoneyBee.prototype = Object.create(Bee.prototype);
      //이 과정을 통해 HoneyBee -> Bee -> Grub의 Scope Chaining이 가능해진다.
    var HoneyBee.prototype.constructor = HoneyBee;

    //3. 상위 객체들로부터 상속 받은 함수들을 HoneyBee에서 사용하기 위해 재정의 하거나 새로운 함수 선언
    HoneyBee.makeHoney = function() {
      this.honeyPot++;
    }

    //4. HoneyBee가 상속 받은 것들을 확인해본다.
    var honeyBee = HoneyBee();
    console.log(honeyBee.age); // 10
    console.log(honeyBee.job); // make honey
    console.log(honeyBee.honeyPot); // 0
    console.log(honeyBee.eat()); // ate -> Grub에서부터 호출됨
    console.log(honeyBee.makeSound()); // weeeing -> Bee에서부터 호출됨
    honeyBee.makeHoney();
    console.log(honeyBee.honeyPot); // 1
  • 속성 값을 상속 받기 위한 객체.call(this, 변수들) 과 함수를 상속 받기 위한 객체.prototype = Object.create(상위 객체) 그리고 객체.prototype.contructor = 객체 자신 의 과정이 매우 중요하다.

2. ES6 Class Subclassing

  • ES6 도입 내용 중 하나인 class 문법은 그간 class라는 개념 없이 prototype의 속성을 이용해 OOP를 구현했던 자바스크립트 유저들에게 더 명료한 문법을 제공했다. 또 다른 OOP 언어 사용자들에게 익숙한 방식으로 상속을 구현할 수 있게 한다... 고 들었다.(나는 자바스크립트만 알아서ㅎㅎㅎ)

  • class 직후 클래스 이름을 선언한 후 스코프 안에 1) constructor 영역 2) method 영역을 담는다. 만약 상위 객체를 상속 받는 하위 객체를 생성하는 과정에서는 class 하위객체이름 extends 상위객체이름 형태로 선언한다. 이전 방법과 달리 class 문법에서는 한 스코프 안에 1)과 2)를 한번에 정의한다.

  • 1) constructor 영역에는 변수명을 담아 this를 통해 속성을 지정한다. 만약 상위 객체로부터 상속 받을 속성값이 있다면 super 라는 키워드를 사용해 super() 혹은 super(일부 변수명)을 선언한다. 하위 객체만 가지는 속성값이 있는 경우에도 이 과정에서 this를 이용해 할당한다.

  • 2) method 영역에는 객체가 사용할 메소드 함수를 정의한다. 이전 방법과 달리 메소드 함수 이름만을 적고 함수의 내용을 담는다. 해당 객체의 속성 값을 사용할 경우 속성 값이 담긴 this를 사용한다. 만약 상위 객체에서 상속 받아 재정의 하거나 활용할 메소드 함수가 있다면 여기서도 super 키워드를 사용해 super(메소드 함수명)을 선언한다.

  • 이 또한 위에서 작성한 같은 코드를 작성하면서 어떤 내용인지 살펴 보겠다. 훨씬 가독성이 올라간 것을 확인할 수 있다.

//1. Grub이라는 객체를 선언하고 속성값과 가지고 있을 함수를 할당한다.
    class Grub {
      constructor() { // 1) constructor 영역
        this.age = 0;
        this.color = 'pink';
        this.food = 'jelly';
      }
      eat() { // 2) method 함수 정의 영역
        return `ate ${this.food}`;
      }
      checkAge() {
        return this.age;
      }
    }

    //2. Grub을 상속 받은 Bee라는 객체를 만들어본다.
    class Bee extends Grub {
      constructor() {
        super(); // 중요: Grub의 모든 속성값을 상속 받는다.

        this.age = 5; // 상속 받은 속성값을 재할당 할 수도 있다.
        this.color = 'yellow';
        this.job = 'grow'; // Bee만 가지는 새로운 속성값도 할당 가능
      }
      makeSound() {
        return `I've made sound weeeing during ${super.checkAge()} years`;
      } // 함수 정의 시 상위 객체의 메소드를 super한 값을 활용할 수 있다.
    }

    //3. HoneyBee라는 객체를 생성하며 Bee의 속성값을 연결한다.
    class HoneyBee extends Bee {
      constructor() {
        super();
        this.age = 10;
        this.job = 'make honey';
        this.honeyPot = 0; // HoneyBee만이 가진 속성값을 정의
      }
      makeHoney() {
        this.honeyPot++;
      }
      checkHoneyPot(goal) {
        return this.honeyPot >= goal ? 'no more work' : 'keep working!';
      }
    }
  • 원형 객체 선언시에는 선언 초반부에 class 객체 {}를, 하위 객체 상속 시에는 class 하위객체 extends 상위객체 로 표현한다.
  • Pseudoclassical에서 하위 객체가 상위 객체의 속성 값을 활용하기 위해 사용했던 상위객체.call(this)라는 표현이 ES6 class 문법에서는 super() 혹은 super(일부 변수명)으로 변경되었다.
    상위 객체의 메소드 함수를 하위 객체에서 활용하기 위해서는 하위 객체의 메소드 함수 내부에 super() 혹은 super.상위객체메소드이름() 을 사용한다.

3. ES6 Subclassing implementation 예시

  • Human(상위 class) -> Student(하위 class) -> sooji(instance)가 있다고 했을 때 상속은 이런 식으로 구현된다.
  • 먼저 Human 이라는 객체 정의를 한다.
    image.png
  • 그리고 Human 객체로부터 상속 받은 Student 객체를 만든다.
    image.png
  • super 키워드를 사용하여 Human의 name, hours 요소를 상속 받았고 sleep 이라는 메소드 함수를 상속 받아 변경(overiding)한다.