ES6
에서 클래스가 도입되기 전까지 ES5
에서는 생성자 함수와 프로토타입을 통해 상속을 구현할 수 있었다.
생성자 함수와 클래스는 몇 가지 차이가 있다.
new
연산자 없이 호출하면 에러가 발생한다. 하지만 생성자 함수를 new
연산자 없이 호출하면 일반 함수로서 호출된다.extends
와 super
키워드를 제공한다.strict mode
가 지정되어 실행된다.클래스는 생성자 함수 기반의 객체 생성 방식보다 견고하고 명료하다.
클래스는 함수로 평가된다.
클래스 선언문으로 정의한 클래스는 함수 선언문과 같이 런타임 이전에 먼저 평가되어 함수 객체를 생성한다.
const Person = '';
{
// 호이스팅이 발생하지 않는다면 ''이 출력되야 한다.
console.log(Person); // // ReferenceError: Cannot access 'Person' before initialization
class Person {}
}
클래스는 클래스 정의 이전에 참조할 수 없다.
console.log(Person); // ReferenceError: Cannot access 'Person' before initialization
class Person {}
class Person {
constructor(name) {
// 인스턴스 프로퍼티
this.name = name;
}
}
const me = new Person('Lee');
console.log(me); // Person {name: "Lee"}
인스턴스 프로퍼티는 반드시 constructor
내부에서 this
에 프로퍼티를 추가해야 한다.
그렇다면 클래스 몸체에 프로퍼티를 선언하면 어떻게 될까?
class Person {
// 인스턴스 프로퍼티 정의
name = 'Lee'
}
const me = new Person();
console.log(me); // Person {name: "Lee"}
클래스 몸체에는 메서드만 선언할 수 있다. 클래스 몸체에 위와 같은 방식으로 인스턴스 프로퍼티를 선언하면 문법 에러가 발생한다.
하지만 위 예제를 최신 브라우저나 Node.js에서 실행하면 문법 에러가 발생하지 않고 정상 동작한다.
🔎 클래스 필드 정의 제안
위와 같은 정의 방식을 클래스 필드 정의라 하는데, 클래스 필드란 클래스 기반의 객체지향 언어에서 클래스가 생성할 인스턴스의 프로퍼티를 가리키는 용어를 말한다.
자바스크립트에서도 인스턴스 프로퍼티를 마치 클래스 필드처럼 정의할 수 있도록 2021년 1월에 제안되어 있다. 아직까진 ECMAScript의 표준으로 승급되진 않았지만 표준 사양으로 승급이 확실시 되어서 최신 브라우저와 최신 Node.js는 이 방식을 미리 구현해놓았다.
여기서 최신 브라우저는 Chrome 72 이상, 최신 Node.js는 버전 12 이상을 말한다.
🚸 클래스 필드 정의 방식에 대한 주의 사항
class Person {
this.name = ''; // SyntaxError: Unexpected token '.'
}
this
에 해당 프로퍼티를 바인딩해서는 안된다. this
는 클래스의 constructor
와 메서드 내에서만 유효하다.
class Person {
name;
}
const me = new Person();
console.log(me); // Person {name: undefined}
초기값을 할당하지 않으면 undefined를 갖는다.
class Person {
name;
constructor(name) {
this.name = name; // 초기화
}
}
const me = new Person('Lee');
console.log(me); // Person {name: "Lee"}
외부의 초기값으로 초기화해야 할 필요가 있다면 constructor
에서 초기화해야 한다.
class Person {
constructor(name) {
this.name = name;
}
}
const me = new Person('Lee');
console.log(me); // Person {name: "Lee"}
사실 인스턴스 프로퍼티를 초기화할 필요가 있다면 constructor
밖에서 정의할 필요가 없다. 어차피 constructor
내부에서 해당 프로퍼티를 참조하여 초기값을 할당해야 하기 때문이다.
클래스는 private
, public
, protected
키워드와 같은 접근 제한자를 지원하지 않는다. 하지만 private
한 프로퍼티를 정의할 수 있는 사양이 현재 제안 중에 있으며, 최신 브라우저와 Node.js는 이미 구현되어 있다.
private
프로퍼티의 선두에는 #
을 붙여준다. private
프로퍼티를 참조할 때도 #
을 붙여 주어야 한다.
class Person {
// private 프로퍼티 정의
#name = '';
constructor(name) {
// private 프로퍼티 참조
this.#name = name;
}
// getter 함수
get name() {
return this.#name;
}
}
const me = new Person('Lee');
console.log(me.#name); // SyntaxError: Private field '#name' must be declared in an enclosing class
console.log(me.name); // 'Lee'
private
프로퍼티인 #name
은 클래스 외부에서는 참조할 수 없다. getter
함수를 통해 간접적으로 접근해야 한다.
class Person {
constructor(name) {
this.#name = name;
// SyntaxError: Private field '#name' must be declared in an enclosing class
}
}
또한 private
프로퍼티는 반드시 클래스 몸체에 정의해야 한다. constructor
에 직접 정의하면 에러가 발생한다.
클래스에는 static
키워드를 사용해 정적 메서드를 정의할 수 있지만, 정적 프로퍼티는 정의할 수 없었다. 하지만 이 또한 가능하도록 현재 제안 중에 있으며, 최신 브라우저와 Node.js는 이미 구현되어 있다.
class MyMath {
// static public 프로퍼티 정의
static PI = 22 / 7;
// static pirvate 프로퍼티 정의
static #num = 10;
// static 메서드
static increment() {
return ++MyMath.#num;
}
}
console.log(MyMath.PI); // 3.142857142857143
console.log(MyMath.increment()); // 11
프로토타입 기반 상속은 프로토타입 체인을 통해 다른 객체의 자산을 상속받는 개념이지만, 상속에 의한 클래스 확장은 기존 클래스를 상속받아 새로운 클래스를 확장하여 정의한다.
class Animal {
constructor(age, weight) {
this.age = age;
this.weight = weight;
}
eat() { return 'eat'; }
move() { return 'move'; }
}
class Bird extends Animal {
fly() { return 'fly'; }
}
const bird = new Bird(1, 5);
console.log(bird); // Bird {age: 1, weight: 5}
console.log(bird instanceof Bird); // true
console.log(bird instanceof Animal); // true
console.log(bird.eat()); // eat
console.log(bird.move()); // move
console.log(bird.fly()); // fly
extends
키워드를 사용한 클래스 확장은 간편하고 직관적이다. 상속을 통해 확장된 클래스를 서브클래스(subclass) 라 부르고 서브클래스에게 상속된 클래스를 수퍼클래스(super-class) 라 부른다.
extends
키워드는 클래스뿐만 아니라 생성자 함수를 상속받아 클래스를 확장할 수 있다. 단, extends
키워드 앞에는 반드시 클래스가 와야 한다.
function Base(a) {
this.a = a;
}
class Derived extends Base {}
const derived = new Derived(1);
console.log(derived); // Derived {a: 1}
또한 조건에 따라 동적으로 상속 대상을 결정할 수도 있다.
function Base1() {}
class Base2 {}
let condition = true;
class Derived extends (condition ? Base1 : Base2) {}
const derived = new Derived();
console.log(derived); // Derived {}
console.log(derived instanceof Base1); // true
console.log(derived instanceof Base2); // false
super
키워드는 함수처럼 호출할 수도 있고 식별자처럼 참조할 수도 있다.
super
를 호출하면 수퍼클래스의 constructor
를 호출한다.
class Base {
constructor(a, b) {
this.a = a;
this.b = b;
}
}
class Derived extends Base {
constructor(a, b, c) {
super(a, b);
this.c = c;
}
}
const derived = new Derived(1, 2, 3);
console.log(derived); // Derived {a: 1, b: 2, c:3}
인수 1, 2, 3은 Derived
클래스의 constructor
에 전달되고, super
호출을 통해 Base
클래스의 constructor
에 일부가 전달된다.
super
를 호출할 때 몇 가지 주의 사항이 있다.
💡 서브클래스에서 constructor
를 생략하지 않은 경우, 반드시 super를 호출해야 한다.
class Base { ... }
class Derived extends Base {
constructor() {
// ReferenceError
console.log('constructor call');
}
}
const derived = new Derived();
💡 서브클래스의 constructor
에서 super
를 호출하기 전에는 this
를 참조할 수 없다.
class Base { ... }
class Derived extends Base {
constructor() {
// ReferenceError
this.a = 1;
super();
}
}
const derived = new Derived(1);
💡 super
는 반드시 서브클래스의 constructor
에서만 호출한다.
class Base {
constructor() {
super(); // SyntaxError: 'super' keyword unexpected here
}
}
function Foo() {
super(); // SyntaxError: 'super' keyword unexpected here
}
메서드 내에서 super
를 참조하면 수퍼클래스의 메서드를 호출할 수 있다.
class Base {
constructor(name) {
this.name = name;
}
sayHi(){
return `Hi! ${this.name}`;
}
}
class Derived extends Base {
sayHi() {
return `${super.sayHi()}. how are you doing?`;
}
}
const derived = new Derived('Lee');
console.log(derived.sayHi()); // Hi! Lee. how are you doing?
서브 클래스의 프로토타입 메서드 내에서 super.sayHi
는 수퍼클래스의 프로토타입 메서드 sayHi
를 가리킨다.
class Rectangle {
constructor(width, height) {
this.width = width;
this.height = height;
}
}
class ColorRectangle extends Rectangle {
constructor(width, height, color) {
super(width, height);
this.color = color;
}
}
const colorRectangle = new ColorRectangle(2, 4, 'red');
console.log(colorRectangle); // ColorRectangle {width: 2, height: 4, color: 'red'}
super
를 호출은 반드시 서브 클래스의 constructor에서 이뤄져야 한다. 서브클래스는 자신이 직접 인스턴스를 생성하지 않고 수퍼클래스에게 인스턴스 생성을 위임하기 때문이다.
서브 클래스가 new 연산자와 함께 호출되면, 서브 클래스 coustructor 내부의 super
키워드가 함수처럼 호출되고 수퍼클래스의 constructor가 호출된다.
// 수퍼클래스
class Rectangle {
constructor(width, height) {
console.log(this); // ColorRectangle {}
...
인스턴스는 수퍼클래스가 생성했지만 new 연산자와 함께 호출된 클래스가 서브클래스라는 것이 중요하다. new 연산자와 함께 호출된 함수를 가리키는 this
은 서브클래스를 가리킨다. 따라서 인스턴스는 서브클래스가 생성한 것으로 처리된다.
class Rectangle {
constructor(width, height) {
console.log(this); // ColorRectangle {}
this.width = width;
this.height = height;
console.log(this); // ColorRectangle {width: 2, height: 4}
}
...
수퍼클래스의 constructor가 실행되어 this
에 바인딩되어 있는 인스턴스를 초기화한다.
class ColorRectangle extends Rectangle {
constructor(width, height, color) {
super(width, height);
console.log(this); // ColorRectangle {width: 2, height: 4}
...
super
호출이 종료되고 서브클래스 constructor에서는 super
가 반환한 인스턴스가 this
에 바인딩된다. 서브클래스는 별도의 인스턴스를 생성하지 않고 super
가 반환한 인스턴스를 this
에 바인딩하여 그대로 사용한다.
this
에 바인딩되어 있는 인스턴스에 프로퍼티를 추가하고 constructor가 인수로 전달받은 초기값으로 인스턴스의 프로퍼티를 초기화한다.
클래스의 모든 처리가 끝나면 완성된 인스턴스가 바인딩된 this
가 암묵적으로 반환된다.
class ColorRectangle extends Rectangle {
constructor(width, height, color) {
super(width, height);
console.log(this); // ColorRectangle {width: 2, height: 4}
this.color = color;
console.log(this); // ColorRectangle {width: 2, height: 4, color: 'red'}
}
}
const colorRectangle = new ColorRectangle(2, 4, 'red');