사용하면서도 왜 그렇게되는지 모르고 사용하는 부분이 등장했다. 예를 들어 서브 클래스 내부에서 constructor를 사용한다면 왜 super를 생략해서는 안되는지 등등.
다시 한번 그리고 제대로 클래스 내부 원리에 대해 공부하고 싶어서 정리한다.
모든 내용을 다 정리할 순 없어 중요하다고 판단되는 부분과 원리를 이해하며 다음부턴 안까먹을 자신이 있는 부분만 정리하겠다.
모든 선언문은 런타임 이전에 먼저 실행된다. var, let, const, function, class 등등. 그리고 이러한 키워드를 통해 선언된 모든 식별자는 호이스팅된다.
함수 선언문 경우에는 소스코드 평가과정 즉, 런타임 이전에 먼저 평가되어 함수 객체를 생성한다. 클래스도 마찬가지이다. 하지만 클래스는 정의 이전에 참조할 수 없다.
console.log(Person);
class Person{}
이는 마치 클래스가 호이스팅이 되지 않는 것처럼 보이지만 아니다. 클래스도 호이스팅이 되지만 let과 const처럼 호이스팅이 되는것이다.
var와 let,const의 차이점이 무엇인가? var는 런타임이전에 평가되고 undefined로 초기화가 된다. 하지만 let,const는 undefined로 초기화되지 않고 값이 들어올 때까지 변수에 참조할 수 없게 된다. 클래스도 마찬가지로 동작한다는 것이다.
정적 메서드는 인스턴스 프로퍼티를 참조할 수 없고 프로토타입은 참조가 가능한 이유는 두 메서드 내부의 this 바인딩이 다르기 때문이다.
class Square{
static area(width, height){
return width * height;
}
}
class Square{
constructor(width, height){
this.width = width;
this.height = height;
}
area(){
return this.width * this.height;
}
}
const square = new Square(10,10);
square.area();
square이 area를 호출한다. 이때 area의 this는 area를 호출한 객체 square를 가리키게된다. 그렇기 때문에 프로토타입 메서드는 인스턴스 프로퍼티에 접근이 가능한 것이다.
하지만 static으로 정의한 area는 프로토타입이 아니라 클래스에 들어있다. 즉, 정적 메서드 내부의 this는 클래스를 가리킨다. 따라서 정적메서드는 인스턴스 프로퍼티에 참조할 수 없다.
이 과정은 뒤에 클래스의 상속에서 서브클래스가 인스턴스를 생성하는 과정을 이해하기 위한 발판이다.
인스턴스 생성과 this 바인딩
new 연산자와 함께 클래스를 호출하면 constructor가 실행되기 이전에 암묵적으로 빈 객체를 만들고 이것이 바로 클래스가 생성한 인스턴스이다. 이것을 this에 바인딩시킨다.
인스턴스 초기화
그 다음에 constructor가 실행되면서 인스턴스 프로퍼티를 추가하거나 외부의 값으로 초기화를 시켜준다.
인스턴스 반환
완성된 인스턴스가 바인딩된 this가 암묵적으로 반환이 된다.
최신 브라우저와 최신 노드환경에서는 클래스 필드를 클래스 몸체에 정의할 수 있다. 다만 몇가지 주의할 점이 있다.
class Person{
this.name = '';
}
class Person{
name = 'Lee';
constructor(){
console.log(name); //❌
}
}
super키워드를 사용할 때 주의할 점 몇가지가 있는데 이를 이해하기 위해서는 상속 클래스의 인스턴스가 어떤 과정에서 생성되는지를 이해하면 정말 쉽다. 암기가 아니라 이해다.
class Rectangle {
constructor(width, height) {
this.width = width;
this.height = height;
}
getArea() {
return this.width * this.height;
}
toString() {
return `width = ${this.width}, height = ${this.height}`;
}
}
class ColorRectangle extends Rectangle {
constructor(width, height, color) {
super(width, height);
this.color = color;
}
toString() {
return super.toString() + `, color = ${this.color}`;
}
}
const colorRectangle = new ColorRectangle(10, 20, 'red');
서브클래스는 자신이 직접 인스턴스를 생성하지 않고 수퍼클래스에게 인스턴스 생성을 위임한다.
이 이유가 바로 서브클래스 constructor에서 반드시 super를 호출해야하는 이유다.
인스턴스를 생성하는 것은 서브클래스가 아니라 수퍼클래스이기 때문이다. "어라 근데 왜 수퍼클래스의 constructor를 호출하는건가요?" 라는 의문이 들었는데 엄밀히 말하자면 수퍼클래스가 평가되어 생성된 함수 객체의 코드가 실행되기 시작하는 것이라고 한다.
여기서 중요한 포인트는 바로 new.target
이다.
class Rectangle {
constructor(width, height) {
console.log(this); // ColorRectangle {}
console.log(new.target); // ColorRectangle
...
인스턴스는 수퍼클래스에서 생성한 것인데 this를 출력하면 서브클래스가 만들었다고 나온다. 그 이유는 new 키워드를 통해 호출한 함수 즉 클래스가 서브클래스이기 때문이다. 인스턴스는 new.target이 가리키는 서브클래스가 생성한 것으로 처리가된다.
수퍼클래스의 인스턴스 초기화
서브클래스 constructor로의 복귀와 this 바인딩
서브 클래스는 부모가 정성스럽게 만들어준 인스턴스를 그냥 가져와서 자신의 this에 바인딩한다. 그렇기 때문에 super가 호출되지 않으면 인스턴스가 생성되지 않고, 그렇게되면 this에 바인딩할 게 없기 때문에 super를 호출하기 이전에 this를 참조할 수 없다.
서브클래스 인스턴스 초기화
인스턴스 반환
따라서 위의 과정을 통해 무엇을 주의해야하는지 모두 알게되었다.
1. 서브 클래스 내에서 constructor를 생략하지 않는 경우 반드시 super를 호출해야한다.
서브클래스의 constructor에서 super를 호출하기 이전에 this에 참조할 수 없다.
super는 반드시 서브클래스의 constructor에서만 호출된다.
super를 참조할 수 있는 곳은 메서드 축약 표현으로 정의된 서브클래스의 메서드이다. 주의사항이라기 보다는 그 이유는 메서드 축약 표현으로 정의된 함수만이 [[HomeObject]] 슬롯을 가지고있기 때문이다.
이와 관련된 자세한 설명은 Deep Dive에 잘 나와있다. 중요한 점은 그 이유를 이해했다는 것이다.