의 상속 - prototype

prototype이 뭔데?

개발을 하다 보면 기존에 있는 기능을 가져와 확장해야 하는 경우가 생긴다.
사람에 관한 프로퍼티와 메서드를 가진 user라는 객체가 있는데, user와 상당히 유사하지만 약간의 차이가 있는 admin과 guest 객체를 만들어야 한다고 가정해 봅시다. 이때 "user의 메서드를 복사하거나 다시 구현하지 않고 user에 약간의 기능을 얹어 admin과 guest 객체를 만들 수 있지 않을까?"라는 생각이 들게 된다.

자바스크립트 언어의 고유 기능인 프로토타입 상속(prototypal inheritance) 을 이용하면 위와 같은 생각을 실현할 수 있다.

prototype은 어떻게 쓰는걸까?

자바스크립트의 객체는 명세서에서 명명한 [[Prototype]]이라는 숨김 프로퍼티를 갖는다. 이 숨김 프로퍼티 값은 null이거나 다른 객체를 참조하는데, 다른 객체를 참조하는 경우 참조 대상을 '프로토타입(prototype)'이라 부른다.

프로토타입의 동작 방식은 '신비스러운’면이 있다. object에서 프로퍼티를 읽으려고 하는데 해당 프로퍼티가 없으면 자바스크립트는 자동으로 프로토타입에서 프로퍼티를 찾기 때문이다. 프로그래밍에선 이런 동작 방식을 '프로토타입 상속’이라 부른다. 언어 차원에서 지원하는 편리한 기능이나 개발 테크닉 중 프로토타입 상속에 기반해 만들어진 것들이 많다.

[[Prototype]] 프로퍼티는 내부 프로퍼티이면서 숨김 프로퍼티이지만 다양한 방법을 사용해 개발자가 값을 설정할 수 있다.

아래 예시처럼 특별한 이름인 __proto__를 사용하면 값을 설정할 수 있습니다.

let animal = {
  eats: true
};
let rabbit = {
  jumps: true
};

rabbit.__proto__ = animal;
  • 객체 rabbit에서 프로퍼티를 얻고싶은데 해당 프로퍼티가 없다면, 자바스크립트는 자동으로 animal이라는 객체에서 프로퍼티를 얻는다.
let animal = {
  eats: true
};
let rabbit = {
  jumps: true
};

rabbit.__proto__ = animal; // (*)

// 프로퍼티 eats과 jumps를 rabbit에서도 사용할 수 있게 되었습니다.
alert( rabbit.eats ); // true (**)
alert( rabbit.jumps ); // true
  • 물론 메서드도 상속이 된다.
let animal = {
  eats: true,
  walk() {
    alert("동물이 걷습니다.");
  }
};

let rabbit = {
  jumps: true,
  __proto__: animal
};

// 메서드 walk는 rabbit의 프로토타입인 animal에서 상속받았습니다.
rabbit.walk(); // 동물이 걷습니다.
  • 상속받은 객체의 프로퍼티를 수정해도 프로토타입의 프로퍼티는 변경되지 않는다.
let user = {
  name: "John",
  surname: "Smith",

  set fullName(value) {
    [this.name, this.surname] = value.split(" ");
  },

  get fullName() {
    return `${this.name} ${this.surname}`;
  }
};

let admin = {
  __proto__: user,
  isAdmin: true
};

alert(admin.fullName); // John Smith (*)

// setter 함수가 실행됩니다!
admin.fullName = "Alice Cooper"; // (**)

alert(admin.fullName); // Alice Cooper , state of admin modified
alert(user.fullName); // John Smith , state of user protected

// admin.fullName = "Alice Cooper";, alert(admin.fullName);
// 위 두 문장처럼 같은 함수명이라고 할지라도 값을 할당하는지, 리턴하는지에 따라 set, get 함수가 결정됨

한 걸음 더 나아가기 - [extends]

  • extends를 이용한 상속
    class Rabbit extends Animal {
      hide() {
        alert(`${this.name} 이/가 숨었습니다!`);
      }
    }
    
    let rabbit = new Rabbit("흰 토끼");
    
    rabbit.run(5); // 흰 토끼 은/는 속도 5로 달립니다.
    rabbit.hide(); // 흰 토끼 이/가 숨었습니다!
  • 상속 메서드 오버라이딩

    개발을 하다 보면 부모 메서드 전체를 교체하지 않고, 부모 메서드를 토대로 일부 기능만 변경하고 싶을 때가 생기는데, 부모 메서드의 기능을 확장하고 싶을 때도 있을것이다. 이럴 때 커스텀 메서드를 만들어 작업하게 되는데, 이미 커스텀 메서드를 만들었더라도 이 과정 전·후에 부모 메서드를 호출하고 싶을 때가 있을것이다.
    키워드 super는 이럴 때 사용한다.
    super.method(...)는 부모 클래스에 정의된 메서드, method를 호출한다.
    super(...)는 부모 생성자를 호출하는데, 자식 생성자 내부에서만 사용 할 수 있다.
    이런 특징을 이용해 토끼가 멈추면 자동으로 숨도록 하는 코드이다.

class Animal {

  constructor(name) {
    this.speed = 0;
    this.name = name;
  }

  run(speed) {
    this.speed = speed;
    alert(`${this.name}가 속도 ${this.speed}로 달립니다.`);
  }

  stop() {
    this.speed = 0;
    alert(`${this.name}가 멈췄습니다.`);
  }

}

class Rabbit extends Animal {
  hide() {
    alert(`${this.name}가 숨었습니다!`);
  }

  stop() {
    super.stop(); // 부모 클래스의 stop을 호출해 멈추고,
    this.hide(); // 숨습니다.
  }
}

let rabbit = new Rabbit("흰 토끼");

rabbit.run(5); // 흰 토끼가 속도 5로 달립니다.
rabbit.stop(); // 흰 토끼가 멈췄습니다. 흰 토끼가 숨었습니다!
  • 상속 생성자 오버라이딩

    상속 클래스의 생성자에선 반드시 super(...)를 호출해야 하는데, super(...)를 호출하지 않으면 에러가 발생한다. super(...)는 this를 사용하기 전에 반드시 호출해야 한다.

    • 일반 클래스가 new와 함께 실행되면, 빈 객체가 만들어지고 this에 이 객체를 할당한다.
    • 반면, 상속 클래스의 생성자 함수가 실행되면, 일반 클래스에서 일어난 일이 일어나지 않는다. 상속 클래스의 생성자 함수는 빈 객체를 만들고 this에 이 객체를 할당하는 일을 부모 클래스의 생성자가 처리해주길 기대한다.
class Animal {

  constructor(name) {
    this.speed = 0;
    this.name = name;
  }

  // ...
}

class Rabbit extends Animal {

  constructor(name, earLength) {
    super(name); // 부모 클래스의 생성자 속 name을 사용하기 위함.
    this.earLength = earLength;
  }

  // ...
}

// 이제 에러 없이 동작합니다.
let rabbit = new Rabbit("흰 토끼", 10);
alert(rabbit.name); // 흰 토끼
alert(rabbit.earLength); // 10

참고 사이트

좋은 웹페이지 즐겨찾기