클래스 상속의 실행 흐름과 this

se-een·2023년 2월 27일
0

JavaScript 분석하기

목록 보기
1/2
post-thumbnail

문제 🚀

class Person {
  age;

  constructor(age) {
    this.age = age;
  }

  printAge() {
    console.log("I don't know!!");
  }
}
class Lee extends Person {
  age;

  constructor(age) {
    super(age);

    this.printAge();
  }

  printAge() {
    console.log('My age is : ' + this.age);
  }
}

new Lee(100);

위 코드의 출력 결과는 무엇일까요? 바로 다음과 같습니다.

My age is : undefined

왜 이런 결과가 나올까요? 😬

실행 흐름 🧩

VSCode의 디버거를 통해 전반적인 실행 흐름과 그에 따른 this 바인딩을 살펴보면서 그 이유를 확인해보겠습니다.

Lee 인스턴스 생성

우선 new Lee(100); 라인을 실행하면서, constructor의 내부 코드가 실행되기 앞서 암묵적으로 빈 객체가 생성됩니다. 이는 Lee 클래스가 생성한 미완성 인스턴스이지요.

이 인스턴스의 프로토타입으로 클래스의 prototype 프로퍼티가 가리키는 객체가 설정됩니다. 이 때 this에 바인딩됩니다. 그래서 constructor 내부의 this는 클래스가 생성한 인스턴스를 가리키게 되는 것이죠.

이후 contructor의 내부 코드가 실행되면서 this에 바인딩되어 있는 인스턴스를 초기화 하면서 인스턴스에 프로퍼티를 추가하고 constructor가 인수로 받은 초기값으로 인스턴스 프로퍼티의 값을 초기화 합니다.

위 사진은 super 를 실행하기 전이므로 아직 미완성된 인스턴스입니다. 따라서 this가 아직 바인딩 되지 않았죠. age : 100age 프로퍼티 값이 아닌 constructor 매개변수의 age 값입니다.

Person 클래스의 constructor 실행

super를 실행하게되면 부모클래스인 Person 클래스의 constructor의 내부 코드를 실행하게 됩니다. 이 과정에서 this 바인딩이 완료되고 age 프로퍼티의 초기값을 초기화하게 됩니다.

주의할 점은 Lee 클래스의 인스턴스임을 잊지 말아야 합니다. Person 클래스의 constructor를 실행하고 있다보니 this에 Person이 바인딩 될 것이란 착각을 할 수도 있는데요. new 키워드로 무슨 클래스의 인스턴스를 생성하였는지 생각하면 헷갈리지 않으실 겁니다.

위에서 설명드린 것과 마찬가지로 age : 100은 constructor의 매개변수 age 값입니다.

생성자 함수의 내부 코드를 모두 완료한 모습입니다. age 프로퍼티에 전달해준 100의 값으로 초기값이 초기화 된 모습입니다.

Lee 클래스의 constructor 실행

부모 클래스의 constructor를 모두 실행하고 오면 다시 본인 클래스의 constructor로 돌아와 남은 코드를 마저 실행하게 됩니다.

무언가 이상한 점을 눈치채셨나요? Person 클래스에서 기껏 초기값을 정해준 age 프로퍼티가 Lee 클래스의 constructor의 내부 코드를 실행하면서 다시 undefined로 초기화 되었습니다.

이는 위에서도 설명드렸다싶이 constructor의 내부 코드가 실행되면서 this에 바인딩 되어 있던 인스턴스를 프로퍼티에 추가하고 constructor의 인수로 전달 받은 초기값으로 인스턴스의 프로퍼티 값을 초기화한다고 했습니다.

Lee 클래스를 보면 age 필드를 다시 재정의하고 있는데요. 즉, Lee 클래스가 생성한 인스턴스에 age 프로퍼티 값이 덮어 씌워지게 되는 것입니다.

Lee 클래스의 constructor에서는 age 프로퍼티의 초기값을 정해주지 않고 있으므로 undefined의 값이 초기화되는 것이죠.

따라서 printAge 메서드를 실행하면 My age is : undefined 라는 결과가 나오게 됩니다.

해결 방법

사실 일반적인 상황은 아닌데요. 이번 로또 미션을 구현하면서 상속을 사용하였고, 필드를 상단에 정의하는 것이 클린 코드의 일부분이기에 무심코 자식 클래스의 필드를 상단에 정의함으로써 undefined 에러 파티를 맛 봤었습니다. 😇

그렇다면 이 문제를 어떻게 해결할 수 있을까요? 사실 굉장히 간단합니다.

  • Lee 클래스의 age 필드를 상단에 정의하지 않는다.
  • Lee 클래스의 constructor 에서 age 필드의 값을 초기화 시켜준다.

Lee 클래스의 constructor에서 age 필드값을 또 초기화 시켜준다면 굳이 상속을 사용하지 않아도 될 것입니다. 따라서 age 필드를 상단에 정의하지 않음으로써 해당 문제를 해결할 수 있습니다.

메서드 오버라이딩 🔎

자바스크립트에서 메서드 오버라이딩은 지원하지만 오버로딩은 지원하지 않는다는 점 다들 아실텐데요. 그렇다면! 맛보기로~ 문제를 하나 더 내보겠습니다.

다음 코드는 어떤 실행 결과를 가져올까요? 🤔

class Person {
  name;

  constructor(name) {
    this.name = name;

    this.printName();
  }

  printName() {}
}
class Lee extends Person {
  name = 'Park';

  constructor(name) {
    super(name);
  }

  printName() {
    console.log('My name is : ' + this.name);
  }
}

new Lee('seeen');

My name is : seeen

위 과정의 흐름을 간략하게 요약하면, 부모 클래스의 메서드가 자식 클래스에서 오버라이딩 되었다면 부모의 메서드 대신 자식의 메서드를 호출하게 되는데요.

그 과정에서 Lee 클래스에 있는 Park 값의 name 필드가 아닌, Person 클래스에 있는 seeen이란 값의 name 필드를 읽어드려 정상적으로 My name is : seeen이란 문구를 출력하게 됩니다.

Person 클래스에서 printName 메서드를 호출하기 직전의 환경

Person 클래스에서 printName 메서드 대신 오버라이딩한 Lee 클래스의 printName을 호출하는 모습

profile
woowacourse 5th FE

0개의 댓글