ES5 이전에서는 프로토타입 기반의 객체 지향 프로그래밍만 가능했다.
새로운 인스턴스를 만들기 위해서는 생성자 함수 내에서 this
키워드와 프로토타입 메서드를 직접 정의해야 했다.
다만 생성자 함수를 통한 인스턴스 생성 방식에서는 new
생성자 없이 함수를 호출하거나, 호이스팅으로 인해 undefined
값이 반환 될 수 있는 등, 프로그래머가 실수할 수 있는 여지가 많았다.
ES6에서 제공되는 class
는 기존 프로토타입 기반 상속을 조금 더 쉽고 명확하게 사용할 수 있도록 해준다.
이로 인해 자바스크립트에서도 일반 객체 지향 언어처럼 쉽게 객체 지향 모델을 사용할 수 있게 되었다.
new
키워드 없이 호출 시 에러 발생extends
, super
키워드를 통한 상속-확장 기능 제공let
, const
와 같이 TDZ를 기반으로 한 동작클래스 역시 함수의 일종으로, 평가를 통해 함수 객체를 생성한다.
하지만 일반 함수와 동일하게 취급되지 않도록하는 내부 슬롯, 내부 메서드, 프로퍼티 등을 갖고 있다.
ES6 기준 클래스 함수 객체는 오직 0개 이상의 메서드를 프로퍼티로 가질 수 있다.
이 메서드는 크게 3종류로 구분된다.
이제 이들에 대해 자세히 알아보자
constructor 몸체 내부의 this
는 생성자 함수의 this
처럼 자신이 생성할 인스턴스를 가리킨다.
// 생성자 함수
function f() {
this.a = "생성자 함수";
}
// 클래스
class C {
constructor () {
this.a = "클래스";
}
}
const a = new f();
const b = new C();
console.log(a.a); // 생성자 함수
console.log(b.a); // 클래스
클래스를 통해 인스턴스를 생성할 때 전달하는 인수는 이 constructor의 매개변수로 전달되어 동작한다.
constructor를 정의하지 않으면 암묵적으로 빈 constructor가 생성된다.
// 아래 두 코드는 같다
class C = {}
class C = {
constructor () {}
}
constructor에서 반환문(return
)을 사용하면 생성자 함수에서 반환문을 사용했을 때와 동일하게 동작한다.
this
가 인스턴스가 된다.constructor 메서드는 별도로 저장되는 것이 아니라, 함수 객체가 생성될 때 기술된 동작을 하도록 함수 객체 코드의 일부가 되어 동작한다.
생성자 함수에서와 달리 class의 몸체에서 ES6의 메서드 축약 표현을 통해 정의되는 메서드들은 모두 프로토타입 메서드가 된다.
즉 C
에서 정의한 메서드는 C
또는 C
가 생성할 인스턴스에 저장되는 것이 아니라 C.prototype
이 참조하는 프로토타입 객체에 저장된다.
정적 메서드는 인스턴스를 생성하지 않고도 직접 접근해 호출할 수 있는 메서드이다.
class 몸체에서 static
키워드와 함께 메서드를 정의할 경우 정적 메서드로 동작한다.
class C {
// ...
static add(a, b) {
return a + b;
}
}
console.log(C.add(5, 10)); // 15
정적 메서드에서는 인스턴스의 프로퍼티를 참조할 수 없다.
즉 인스턴스의 프로퍼티에 접근하여 로직을 처리하는 메서드의 경우 프로토타입 메서드로 사용해야 한다.
인스턴스의 프로퍼티가 필요 없거나, 네임스페이스 역할을 하는 클래스가 필요할 경우, 정적 메서드를 이용하면 새로운 인스턴스를 생성할 필요가 없이 메서드를 바로 사용할 수 있다.
이 경우 인스턴스를 생성하는 비용이 들지 않고 메모리 측면에서 보다 효율적이다.
빌트인 함수 중 Math.ceil()
, Math.floor()
와 같은 메서드들이 정적 메서드에 해당한다.
클래스 몸체 내부에서 프로토타입 메서드로 getter
, setter
를 정의할 경우, 인스턴스의 접근자 프로퍼티로 사용할 수 있다.
class C {
constructor (firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
get fullName() {
return `${this.firstName} ${this.lastName}`;
}
set fullName(fullName) {
const [firstName, lastName] = fullName.split(" ");
this.firstName = firstName;
this.lastName = lastName;
}
}
const me = new C("cj", "kang");
console.log(me.fullName); // cj kang
me.fullName = "cjkang me";
console.log(me.fullName); // cjkang me
클래스 필드란 클래스가 생성할 인스턴스의 프로퍼티를 가리키는 용어이다.
ECMAScript2022에서 클래스 필드를 constructor 몸체 밖에서 선언할 수 있는 기능이 추가되었다.
// ES6
class OlderC {
constructor () {
this.a = "a";
}
}
// ECMAScript2022
class NewerC {
a = "a";
}
const older = new OlderC();
const newer = new NewerC();
console.log(older.a); // a
console.log(newer.a); // a
ES6에서는 모든 인스턴스의 프로퍼티는 public 프로퍼티로, 클래스 외부나, 서브 클래스에서 접근할 수 있었다.
ECMAScript2022에서는 프로퍼티의 이름 앞에 #
을 붙일 경우, private 필드로 사용되어 클래스 외부, 서브 클래스에서 접근할 수 없게 되었다.
인스턴스 프로퍼티를 private
로 하고 싶은 경우 constructor
몸체 밖에서 먼저 선언해야 한다.
class C {
#x;
constructor (x) {
this.#x = x;
}
print() {
console.log(this.#x);
}
};
const a = new C("a");
const b = new C("b");
a.print(); // a
b.print(); // b
console.log(a.#x); // error
... 이어야 하는데 크롬 개발자 도구 기준으로 a.#x
가 참조가 된다!
다른 쪽으로 오류를 발생시켜보면
VM1007:26 Uncaught SyntaxError: Private field '#x' must be declared in an enclosing class
이렇게 Private field로 정상 취급하는 것을 알 수 있는데...
왜 이런지는 추후 더 조사해보아야 할 것 같다.
그리고 정적 메서드, 정적 필드 역시 private 필드로 정의가 가능하다.
static
키워드 이후 식별자 앞에 #
을 붙이면 된다.
extends
키워드를 통해 자바스크립트에서도 쉽게 기존 클래스를 상속받아 확장된 클래스를 정의할 수 있게 되었다.
이때 상속의 대상이 되는 클래스를 수퍼 클래스 또는 부모 클래스라고 한다.
상속을 받아 확장되는 클래스를 서브 클래스 또는 자식 클래스라고 한다.
class Game {
constructor(name, genre) {
this.name = name;
this.genre = genre;
}
play () {
// ...
}
}
class Switch extends Game {
constructor (name, genre, user) {
super(name, genre);
this.user = user;
}
charge () {
// ...
}
}
const mario = new Switch( ... )
위 클래스의 구조를 그려보면 다음과 같다.
prototype
이 가리키는 프로토타입 객체에 저장되어 서브 클래스와 인스턴스로 상속되는 프로토타입 체인을 이룬다.this
에 추가되는 인스턴스 프로퍼티는 생성된 인스턴스가 직접 갖고 있는다.class SubClass extends SuperClass
위와 같이 상속받을 클래스를 extends
키워드 다음에 명시하면 된다.
사실 extends
키워드 다음에 반드시 클래스가 와야하는 것은 아니다.
정확히는 [[constructor]]
를 내부 메서드로 갖고 있는 함수 객체, 또는 해당 함수 객체로 평가되는 표현식이 올 수 있다.
// 가능한 예시
class Super1 {
constructor () {
this.a = "a"
}
}
function Super2() {
this.a = "b"
}
class C extends (조건문 ? Super1 : Super2) {
// ...
}
위 예시에서 조건문이 true일 경우 인스턴스의 a
프로퍼티는 "a"가 되며, 조건문이 false 일 경우는 "b"가 된다.
위 예시 코드에서 서브 클래스에서 super()
를 통해 super
를 호출하고 있는 것을 알 수 있다.
super
는 서브 클래스에서 수퍼 클래스로 접근할 수 있는 키워드 이다.
super
는 참조하거나 호출할 수 있으며, 각각 다르게 동작한다.
super() - 호출
: 수퍼 클래스의 constructor를 호출super - 참조
: 수퍼 클래스의 메서드 등을 참조서브 클래스의 constructor에서는 반드시 수퍼 클래스의 constructor를 호출하여야 한다. (그렇지 않으면 상속이 이루어지는 것이 아니므로)
서브 클래스에서 constructor를 생략할 경우 암묵적으로 수퍼 클래스의 constructor를 호출하게 된다.
그러나 constructor를 생략하지 않는다면 super()
를 통해 명시적으로 수퍼 클래스의 constructor를 호출해야 한다.
this
참조 이전에 호출이 이루어져야 한다. (에러 발생)this
가 먼저 참조되면 안되는 이유는 서브 클래스에서는 this
와 바인딩 될 인스턴스를 생성하지 않기 때문이다.super()
를 호출할 수 있다. (참조는 몸체 밖에서 가능)super
로 수퍼 클래스를 참조하기 위해서는 ES6의 메서드 축약 표현으로 정의된 메서드여야 한다.super
가 수퍼 클래스의 프로토타입 객체를 참조해야 하는데, 이 때 [[HomeObject]]
내부슬롯에 저장된 자신을 바인딩하고 있는 객체(서브 클래스의 프로토타입)를 이용하기 때문이다. 메서드 축약 표현으로 정의된 메서드만 [[HomeObject]]
내부 슬롯을 갖는다.class Game {
constructor(name, genre) {
this.name = name;
this.genre = genre;
}
play () {
// ...
}
}
class Switch extends Game {
constructor (name, genre, user) {
super(name, genre);
this.user = user;
}
charge () {
// ...
}
}
const mario = new Switch( ... )
클래스(+ 생성자 함수) 함수 객체에는 [[ConstructorKind]]
라는 내부 슬롯이 존재한다.
상속 받는 것이 없는 기반(base) 클래스라면 이 내부 슬롯은 "base"
를 값으로 갖고, 상속 받는 것이 있다면 "derived"
를 값으로 갖는다.
이를 통해 클래스가 다른 클래스를 상속 받은지 여부를 알 수 있는 정보가 담겨 있다.
super()
를 이용한다.여기서는 서브 클래스인 Switch
객체가 호출되었으므로 후자의 동작을 하게 된다.
수퍼 클래스의 constructor에서 암묵적으로 빈 객체를 생성하고, this
에 바인딩한다.
객체 생성 자체는 수퍼 클래스에서 이루어지지만, 인스턴스의 프로토타입은 서브 클래스의 프로토타입을 가리켜야하므로, 서브 클래스가 생성한 것처럼 내부적으로 처리된다.
constructor 몸체의 코드를 이용해 인스턴스에 프로퍼티를 추가한다.
이 때는 빈 객체를 생성하는 과정 없이 수퍼 클래스가 반환한 객체(인스턴스)에 this를 바인딩한다.
즉, super()
가 호출 되어야 인스턴스 객체가 생성되는 것이기 때문에 this
참조 이전에 super()
가 선언되어야 한다.
수퍼 클래스로부터 넘겨받은 인스턴스에 프로퍼티를 추가한다.