25장 클래스

niyu·2021년 7월 13일
0
post-thumbnail

클래스의 도입

ES6에서 클래스가 도입되기 전까지 ES5에서는 생성자 함수프로토타입을 통해 상속을 구현할 수 있었다.

생성자 함수와 클래스는 몇 가지 차이가 있다.

  • 클래스를 new 연산자 없이 호출하면 에러가 발생한다. 하지만 생성자 함수를 new 연산자 없이 호출하면 일반 함수로서 호출된다.
  • 클래스는 상속을 지원하는 extendssuper 키워드를 제공한다.
  • 클래스 내 코드는 암묵적으로 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 프로퍼티

클래스는 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 프로퍼티

클래스에는 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 키워드는 함수처럼 호출할 수도 있고 식별자처럼 참조할 수도 있다.

1. 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
}

2. super 참조

메서드 내에서 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'}

📚 1. 서브 클래스의 super 호출

super를 호출은 반드시 서브 클래스의 constructor에서 이뤄져야 한다. 서브클래스는 자신이 직접 인스턴스를 생성하지 않고 수퍼클래스에게 인스턴스 생성을 위임하기 때문이다.

서브 클래스가 new 연산자와 함께 호출되면, 서브 클래스 coustructor 내부의 super 키워드가 함수처럼 호출되고 수퍼클래스의 constructor가 호출된다.

📚 2. 수퍼 클래스의 인스턴스 생성과 this 바인딩

// 수퍼클래스
class Rectangle {
  constructor(width, height) {
    console.log(this); // ColorRectangle {}
 ...

인스턴스는 수퍼클래스가 생성했지만 new 연산자와 함께 호출된 클래스가 서브클래스라는 것이 중요하다. new 연산자와 함께 호출된 함수를 가리키는 this은 서브클래스를 가리킨다. 따라서 인스턴스는 서브클래스가 생성한 것으로 처리된다.

📚 3. 수퍼클래스의 인스턴스 초기화

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에 바인딩되어 있는 인스턴스를 초기화한다.

📚 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에 바인딩하여 그대로 사용한다.

📚 5. 서브클래스의 인스턴스 초기화 및 반환

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');

0개의 댓글