❓클래스란
자바스크립트는 프로토타입 기반으로 객체지향 프로그래밍을 구현한다. 따라서 클래스 없이도 자바스크립트는 객체지향 언어의 상속을 구현하는 것이 가능하다. 하지만 ES6에서 기존 다른 언어에서 객체지향 프로그래밍을 구현하기 위해 클래스를 사용한 것처럼 자바스크립트에도 클래스가 도입되었다.
1️⃣ 클래스 문법
클래스 정의
클래스는 class 키워드를 사용하여 정의하고, 생성자 함수처럼 파스칼 케이스를 사용하는 것이 일반적이다. (파스칼 케이스를 사용하지 않아도 에러가 발생하지는 않는다.) 클래스 몸체에서 정의할 수 있는 3가지 메서드에 대해서도 알아보도록 하자.
class User {} // 클래스 선언문
const User = class {} // 익명 클래스 표현식
const User = class MyClass {} // 기명 클래스 표현식
클래스는 일반적으로 선언문을 통해 정의하지만, 표현식으로도 정의하는 것이 가능하다. 따라서 클래스도 값으로 사용할 수 있는 일급 객체이고 클래스는 함수다.
class User {}
const user1 = new User();
클래스의 유일한 존재이유는 인스턴스의 생성이다. 생성자 함수를 통해 인스턴스를 생성한 것과 동일하게 클래스를 통한 인스턴스 생성도 위와 같이 new 연산자와 함께 호출한다.
클래스 메서드
클래스 몸체에서 정의할 수 있는 3가지 메서드는 constructor, 프로토타입 메서드, 정적 메서드 이렇게 세가지가 존재한다. 예제를 통해 살펴보자.
constructor
클래스의 유일한 존재이유는 인스턴스의 생성이라고 했다. constructor는 그 인스턴스를 생성하고 초기화 하는 메서드로 new 연산자를 통해 클래스를 호출하면 constructor 메서드는 자동으로 호출된다. 클래스를 정의할 때 constructor 메서드를 생략한다면 빈 constructor 메서드가 암묵적으로 정의되고, 해당 constructor 메서드는 빈 객체를 생성하게된다. 추가적으로 constructor 메서드는 클래스 내에 최대 한 개만 있어야한다.
class User1 {
constructor(name) {
this.name = name
}
}
const user1 = new User1("Kim");
console.log(user1); // User1 { name: 'Kim' }
class User2 {}
const user2 = new User2();
console.log(user2); // User2 {}
따라서 생성한 인스턴스와 동시에 해당 인스턴스의 초기화를 진행하기 위해서는 해당 constructor 메서드를 반드시 명시해서 사용해야한다.
프로토타입 메서드
생성자 함수에서는 프로토타입 메서드를 추가하기 위해서는 직접 명시적으로 프로토타입 메서드를 추가해주어야 했다. 하지만 클래스 프로토타입 메서드는 클래스 내부 몸체에서 정의한 메서드가 기본적으로 프로토타입 메서드가 된다.
class User {
constructor(name) {
this.name = name
}
greeting() {
console.log(`Hi! ${this.name}`)
}
}
const user1 = new User("Kim");
따라서 생성한 인스턴스 user1은 프로토타입 메서드인 greeting을 상속받아 사용하는 것이 가능하다.
정적 메서드
정적 메서드는 클래스에 바인딩하는 메서드로 생성한 인스턴스를 생성하지 않고도 호출하는 것이 가능한 메서드이고, static 키워드를 통해 정적 메서드를 구현할 수 있다.
class User {
constructor(name) {
this.name = name
}
greeting() {
console.log(`Hi! ${this.name}`)
}
static hello() {
console.log('Hello')
}
}
const user1 = new User("Kim");
User.hello() // Hello
클래스 필드
자바스크립트의 클래스 몸체에서는 위의 세가지 메서드만 정의하는 것이 가능하지만, Chrome 72이상의 최신 브라우저 또는 Node.js 버전 12 이상의 환경에서는 메서드 뿐만 아니라 클래스 필드를 정의하는 것이 가능하다.
class User {
name = "Kim"; // public
#address = "seoul"; // private
getName = function () {
console.log(this.#address);
};
}
const user1 = new User();
console.log(user1); // User { name: 'Kim', getAge: [Function: getAge] }
user1.getName(); // seoul
클래스 필드는 위와 같이 초기화하는 것이 가능하고, 초기값을 할당하지 않는다면 undefined 값을 갖는다. 만약 클래스 필드를 정의하고 인스턴스 생성시 해당 클래스 필드를 초기화하기 위해서는 constructor 메서드에서 해당 클래스 필드를 초기화 해야한다. 따라서 인스턴스 생성시에 클래스 필드를 초기화해야 한다면 그냥 constructor 메서드만 사용하는 편이 낫다.
단, #를 클래스 필드 앞에 붙여 private 필드를 정의하기 위해서는 반드시 클래스 몸체에 정의해야한다.
class User1 {
#address = ''
constructor(address) {
this.#address = address
}
}
const user1 = new User1('seoul');
class User2 {
constructor(address) {
this.#address = address
}
}
const user2 = new User2('seoul'); // 에러 발생
또한, 함수는 일급 객체이므로 함수 또한 클래스 필드에 할당하는 것이 가능하고, 위의 방법으로 클래스 필드를 정의하는 것들은 모두 인스턴스의 프로퍼티 또는 인스턴스 메서드로 동작한다.
2️⃣ 클래스 상속
클래스 상속은 기존 클래스를 상속받아 새로운 클래스를 확장하여 정의하는 것으로 extends 키워드를 통해 상속을 구현한다.
자바스크립트의 엔진은 클래스 평가시에 수퍼클래스(부모클래스)와 서브클래스(자식클래스)를 구분하기 위해 base, derived를 값으로 갖는 내부 슬롯 [[ConstructorKind]]를 갖는다. 다른 클래스를 상속받지 않는 클래스는 해당 내부 슬롯의 값이 base지만, 다른 클래스를 상속받는 클래스는 해당 내부 슬롯의 값이 derived이다.
클래스를 상속받은 서브 클래스는 직접 인스턴스를 생성하는 것이 아니라, 수퍼클래스에서 생성된 인스턴스가 반환되면 해당 인스턴스를 this에 바인딩하여 그대로 사용한다. 따라서 super가 호출되지 않으면 인스턴스가 생성되지 않고 this 또한 반환되지않아 서브클래스의 constructor 메서드 내부에서 super 호출 전 this를 참조하는 것이 불가능하다.
따라서 서브 클래스의 constructor 메서드에서 인스턴스 초기화는 super 호출 이후에 이루어져야 한다.
class Transport {
constructor(speed) {
this.speed = speed;
}
go() {
console.log("출발합니다.");
}
stop() {
console.log("멈춥니다.");
}
}
class Car extends Transport {
constructor(speed, price, color) {
super(speed);
this.price = price;
this.color = color;
}
go() {
console.log("자동차가 출발합니다.");
}
windowOpen() {
console.log("창문을 엽니다.");
}
}
const car = new Car(120, 20000, "red");
console.log(car); // Car { speed: 120, price: 20000, color: 'red' }
car.go(); // 자동차가 출발합니다.
Transport 클래스를 상속받은 Car 클래스를 통해 생성된 인스턴스의 프로토타입 체인은 다음과 같다.