[JS] 상속관계에 대한 고찰

Jiheon Kim·2023년 12월 18일
0

Javascript

목록 보기
5/6
post-thumbnail

💡 시작

자바스크립트 관련 책을 보다가 문득 자바스크립트의 상속관계에 대한 궁금증이 생겼다 과연 과거의 자바스크립트는 상속관계를 어떻게 표현했을까?

ES6 이후의 모던 자바스크립트에선 class 문법을 지원하기 때문에 extends를 통해 간단하게 상속관계를 구현이 가능하다

💡 ES6 이후

class Parent {
  constructor(name) {
    this.name = name;
  }
  introduce() {
    console.log(`Hi, I'm ${this.name}.`);
  }
}

class Child extends Parent {
  constructor(name, age) {
    super(name);
    this.age = age;
  }
}

하지만 ES6이전의 자바스크립트 또는 그 이전의 자바스크립트에선 어떻게 이러한 상속관계를 표현할 수 있었을까?

우선 클래스가 없었으니 생성자함수로 바꾸고 메소드를 프로토타입에 추가해야 할 텐데
상속관계를 표현할 때 제일 먼저 떠오른 생각은 비표준속성 __proto__ 였다

💡 ES6 이전

function Parent(name) {
  this.name = name;
}

Parent.prototype.introduce = function () {
  console.log(`Hi, I'm ${this.name}.`);
};

function Child(name, age) {
  this.name = name;
  this.age = age;
}

Child.prototype.__proto__ = Parent.prototype;

이렇게 하면 생성한 Child 인스턴스 프로토타입의 체인이 Parent.prototype 을 가리키게 되고 결과적으로 Parent의 프로토타입에 있는 메소드들을 상속받게된다.

✔️ __proto__ 속성과 내부슬롯 [[prototype]]

모든 객체는 [[prototype]] 이라는 내부슬롯이 존재하고 이 내부슬롯의 값은 프로토타입 체인상의 자신의 상위 프로토타입을 가리키는데 비표준속성 __proto__ 프로퍼티는 이러한 [[prototype]] 내부슬롯이 가리키는 프로토타입에 간접적으로 접근 할 수 있게 해준다.

__proto__속성은 대부분의 브라우저에서 사용가능 하지만 비표준속성이라서 권장하지 않고 무엇보다 표준이된 getPrototypeOf()setPrototypeOf()가 있기떄문에 사용하지 않는다. 그렇다면 표준 메소드를 사용해서 상속관계를 만들면되지 않을까?

Object.setPrototypeOf(Child.prototype, Parent.prototype)

안타깝지만 표준이된 Object.setPrototypeOf()ES6에서 추가된 메소드이다
그래서 ES6에서는 그냥 클래스를 사용하면 될뿐더러 지금은 ES6 이전의 상속관계를 말하고 있기 때문에 이 방법 또한 적합하지 않다.

💡 ES5

그렇다면 ES5에서는 상속관계를 어떻게 표현했을까?

function Parent(name) {
    this.name = name;
  }
  
  Parent.prototype.introduce = function () {
    console.log(`Hi, I'm ${this.name}.`);
  };
  
  function Child(name, age) {
    Parent.call(this, name);
    this.age = age;
  }
  
  Child.prototype = Object.create(Parent.prototype);
  Child.prototype.constructor = Child;

찾아보니 ES5에서는 ES5에 추가된 프로토타입 객체를 명시적으로 지정해서 새 객체를 만드는 Object.create() 메소드를 통해서 상속관계를 표현한다. 또한 Child.prototypeconstructor 속성이 Parent를 가리키고 있기 때문에 constructor을 원래 값인 Child로 바꿔주며, 재밌던 점은 Parent.call(this, name); 이 부분을 통해서 부모의 생성자 함수를 호출해서 마치 super(name) 처럼 사용한다는 점이었다

✔️ prototype.contructor 에서 헷갈렸던 점

  Child.prototype = Object.create(Parent.prototype);
  console.log(Child.prototype.constructor); // [λ: Parent]

  new Child()  // ?? 

create() 메소드로 Parent 프로토타입을 갖는 새로운 객체를 Child.prototype으로 할당했는데 새로 생성된 객체는 실제로 값은 비어있어서 Child.prototype.contructor 속성으로 접근하면 프로토타입 체인을 따라가면서 Parent.prototype 에서 constructor 속성을 찾게 되어 결과적으로 Child.prototype.constructor의 값은 Parent를 가리킨다.

따라서 constructor 속성으로 미루어보아 Child의 생성자가 Parent 이기 때문에 constructor 속성이 new Child()를 할 때 발생하는 생성자함수의 트리거를 결정하는 속성인가 했는데 결과적으로 constructor 속성은 단순히 생성자 함수를 가리키는 속성이며, 이를 변경한다고 해서 실제로 생성자 함수가 강제로 실행되는 것은 아니었다

💡 ES5 이전

여기까지 오니까 ES5이전에는 상속관계를 어떻게 표현했을지 궁금해졌다
ES4는 중간에 개발이 중단되어서 취소되었기 때문에 시기적으로는 ES3가 된다

function Parent(name) {
    this.name = name;
  }
  
Parent.prototype.introduce = function () {
    console.log(`Hi, I'm ${this.name}.`);
};
  
function Child(name, age) {
    Parent.call(this, name);
    this.age = age;
}

Child.prototype = new Parent()

꽤나 흥미로운 방법이었다 new Parent()Parent의 인스턴스를 Child.prototype에 할당하므로 Child 인스턴스는 Parent의 메소드를 상속받으면서도, Parent의 프로토타입을 수정하면 해당 변경이 반영되게 된다. 하지만 이 방법은 Child 인스턴스를 만들 때마다 Parent의 생성자 함수가 실행되어 매번 Parent 인스턴스를 만들게된다

또 다른 방법으로는 결과론적 일 수 있지만 Object.create() 폴리필 하면된다
하지만 이 방법 또한 역시 내부적으로는 Child.prototype = new Parent()이 사용될 것이다

💡 ES3 이전

Function.prototype.call() 메소드가 ES3에 추가된 메소드라는데 그렇다면 ES3이전의 자바스크립트 즉, 거의 초창기의 자바스크립트는 상속관계를 어떻게 표현했을까?

function Parent(name) {
    this.name = name;
  }
  
Parent.prototype.introduce = function () {
    console.log(`Hi, I'm ${this.name}.`);
};
  
function Child(name, age) {
    this.name = name
    this.age = age;
}

Child.prototype = new Parent()

ES3 이전에는 ES3와 유하자지만 call() 함수가 없으니까 직접 부모의 프로퍼티를 복사하여 상속을 흉내 냈던 것 같다

💡 마무리

우연히 자바스크립트의 상속관계를 찾아보다가 과거에는 어땠을까? 라는 의문으로 계속 파고들면서 내용을 정리해봤는데 꽤나 재미있었다. 이렇게 보고 나니까 ES6에서 추가된 클래스 문법이 굉장히 쉽고 직관적이라고 다시한번 느꼈고 동작원리를 찾으면서 프로토타입에 대해서 좀 더 고민하고 정리하는 기회였던 것 같다

profile
누군가는 해야하잖아

0개의 댓글