자바스크립트는 프로토타입 기반의 객체지향 언어로, 프로토타입을 기반으로 클래스 기반 언어의 클래스를 구현할 수 있었지만 ES6에서 클래스가 도입되었다.
클래스는 new 연산자와 함께 호출되어 인스턴스를 생성하고, constructor, 프로토타입 메서드, 정적 메서드를 가진다. 호이스팅이 일어나지만 let, const처럼 TDZ에 위치한다.
class Square {
1. 암묵적인 빈 객체(인스턴스) 생성, this에 바인딩
constructor (width,height) { //constructor, 인스턴스 생성시 호출됨
2. this에 바인딩 된 인스턴스 초기화
this.width = width;
this.height = height;
}
area() { //프로토타입 메서드
return this.width * this.height;
}
static areaT(width,height) { //정적 메서드
return width * height;
3. 완성된 인스턴스가 바인딩된 this 암묵적 반환
}
const square = new Square(10,10);
console.log(square.area()); // 인스턴스로 호출
console.log(Square.areaT(10,10); // 클래스로 호출
차이점이 있다면, 정적 메서드의 this 바인딩은 클래스, 프로토타입 메서드의 this 바인딩은 생성할 인스턴스라는 점이 있고, 호출하는 방식이 다르다. 그리고 정적 메서드는 인스턴스 프로퍼티를 참조할 수 없다.
크게 인스턴스 프로퍼티, 접근자 프로퍼티로 나눌 수 있고, 클래스 필드 정의, private, static 필드 정의가 있다.
먼저 인스턴스 프로퍼티는 constructor 내부에서 정의하며, 생성될 인스턴스의 프로퍼티이며 public이다. 접근자 프로퍼티는 이전에 알아본 get, set과 유사하다.
클래스 기반 언어에서 클래스가 생성할 인스턴스의 프로퍼티를 클래스 필드라고 하는데, 자바스크립트에서는 다른 언어들과 다르게 반드시 constructor 내부의 this에 추가해야 했다. 현재는 다른 언어들처럼 사용할 수 있으나 주의사항이 있다.
class Person {
name = 'Kim';
#nameI = 'Lee';
static nameS = 'Park';
constructor() {
console.log(this.name);
}
}
const me = new Person();
console.log(me); //Person {name : 'Lee'}
이렇게 this 없이 정의하며 참조는 this를 붙여서 사용한다. 모든 클래스 필드는 인스턴스 프로퍼티가 되므로 클래스 필드에 함수를 할당하는 것은 추천하지 않는다. (메서드를 정의하고 싶다면)
그리고 클래스 필드 앞에 #를 붙이면 private 필드로 정의되며 클래스 내부에서만 참조할 수 있다. (constructor에 직접 정의는 불가능)
static 를 붙인다면 정적 필드로 정의된다.
클래스의 핵심적이고 강력한 기능은 상속이다. extends 키워드로 클래스를 확장하며 super 키워드로 부모 클래스의 constructor과 메서드를 호출한다.
class Base {
constructor(age, weight) {
this.age = age;
this.weight = weight;
}
eat() { return 'eat'; }
}
class Derived extneds Base {
fly() { return 'fly'; }
}
여기서 Derived 클래스는 constructor를 호출하지 않았다. 그런 경우 암묵적으로 constructor(...args) { super(...args); } 가 호출되어 인스턴스를 생성한다. 그렇다면 여기서 super가 무엇일까? super를 호출하면 부모 클래스의 constructor를 호출하고 참조하면 부모 클래스의 메서드를 참조할 수 있다.
class Base {
constructor(age, weight) {
this.age = age;
this.weight = weight;
}
eat() { return 'eat'; }
}
class Derived extneds Base {
constructor(age, weight, height) {
super(age,weight);
this.height = height;
}
toEat() {
return `&{supere.eat()}`
}
}
super는 이렇게 사용되며, 프로토타입 메서드에서 사용시 부모 클래스의 프로토타입 메서드를, 정적 메서드에서 사용시 부모 클래스의 정적 메서드를 참조한다. 메서드는 내부 슬롯 [[HomeObject]]를 가지는데, 이것은 자신을 바인딩하고 있는 객체를 가리키며
super는 super = Object.getPrototypeOf([[HomeObject]]) 와 같다고 생각하면 된다.
과정을 좀 더 자세하게 알아보자.
js는 부모와 자식클래스를 구분하는 내부 슬롯이 존재한다. 자식 클래스가 호출되면, 인스턴스 생성을 부모 클래스에세 위임하기 위해 먼저 constructor를 찾는다. (없다면 super를 호출할 것, 그러므로 constructor가 존재한다면 그 안에 반드시 super 호출이 있어야 함) 이후 부모 클래스로 가 클래스 생성 과정을 실행하고 (여기서 생성은 부모에게 위임했지만 생성은 자식이 한 것으로 취급되어 new.target는 자식클래스를 가리킨다), 다시 돌아와 반환된 인스턴스를 this에 그대로 바인딩하여 사용한다. 이런 구조이므로 자식 클래스에서는 super 호출 전 this를 참조할 수 없다.