4장 클래스

2023.06.16

4.1 클래스란 무엇인가?

  • 자바스크립트에는 실제로 클래스가 없다. 그렇지 않은가? 자바스크립트는 단지 프로토타입으로 클래스를 모사한다. 맞는 말인가?
  • 클래스는 대체로 언어가 제공하는 정적인 구조 이상이다.
  • 클래스를 갖는다는 것은 클래스 기반과 같지 않으며 언어가 캡슐화 및 상속을 지원한다는 의미이다.
  • 프로토타입 언어는 자바스크립트가 발명되기 전부터 클래스를 가질 수 있었다.
  • 자바스크립트는 항상 여러분이 찾을 수 있는 가장 객체 지향적인 언어이다.
  • ES5가 상속을 직접 지원하기 위해 Object.create를 추가할 때까지 컴퓨터 과학 관점에서는 클래스가 없다고 주장할 수 있다.
  • ES5조차도 선언적 구조와 슈퍼클래스 메서드를 참조하는 간단한 방법이 없기 때문에 자격이 없다고 주장할 수도 있다.

4.2 새로운 클래스 문법 소개

  • 아래 코드는 class의 기본적인 예를 보여준다.
    • 생성자
    • 세 가지 데이터 속성(r, g, b)
    • 접근자 속성
    • 프로토타입 메서드(toString, 일반적으로 인스턴스를 통해 접근하기 때문에 인스턴스 메서드라고도 하지만 프로토타입 메서드가 더 정확하다. 실제 인스턴스 메서드는 프로토타입에서 상속되지 않고 인스턴스에만 존재한다)
    • 정적 메서드(fromCSS, 종종 클래스 메서드라고도 한다)
1class Color {
2  constructor(r = 0, g = 0, b = 0) {
3    this.r = r;
4    this.g = g;
5    this.b = b;
6  }
7
8  get rgb() {
9    return 'rgb(' + this.r + ', ' + this.g + ', ' + this.b + ')';
10  }
11
12  set rgb(value) {
13    // ...code shown later...
14  }
15
16  toString() {
17    return this.rgb;
18  }
19
20  static fromCSS(css) {
21    // ...code shown later...
22  }
23}
24
25let c = new Color(30, 144, 255);
26console.log(String(c)); // "rgb(30, 144, 255)"
  • 함수와 마찬가지로 선언 또는 표현식으로 클래스를 정의한다.
1// 클래스 정의
2class Color {}
3
4// 익명 클래스 정의
5let Color = class {};
6
7// 클래스 정의
8let C = class Color {};
  • 클래스 선언은 함수 선언처럼 호이스팅되지 않는다.
  • 임시 데드존으로 완성된 let과 const처럼 초기화가 아닌 식별자만 호이스팅된다.
  • let과 const와 마찬가지로 전역 스코프에서 클래스를 선언하면 클래스의 식별자는 전역이지만 전역 객체의 속성이 아니다.

4.2.1 생성자 추가

생성자 정의의 닫는 중괄호 뒤에는 세미콜론이 없다. 클래스 본문의 생성자 및 메서드 정의는 선언과 비슷하며 뒤에 세미콜론이 없다(세미콜론은 존재하는 경우 허용된다. 문법은 이러한 쉬운 실수를 구문 오류로 만들지 않도록 특별히 허용한다).

  • 생성자를 제공하지 않으면 자바스크립트 엔진은 마치 클래스에 있는 것처럼 정확하게 아무것도 하지 않는 생성자를 생성한다.
  • 생성자는 함수이지만 객체 생성 프로세스의 일부로만 호출할 수 있다.
  • 생성자 함수를 실행하려면 new를 사용한 결과 또는 Reflect.construct 호출의 결과이어야 한다.
  • 객체를 생성하지 않을 때 호출하려고 하면 오류가 발생한다.
1Color(); // TypeError: Class construct Color cannot be invoked without 'new'
  • ES5에서는 아래와 같이 new를 통한 호출을 제외하고는 호출을 허용하도록 할 수 있다.
1var Color = function Color(r, g, b) {
2  if (!(this instanceof Color)) {
3    throw new TypeError("Class construct Color cannot be invoked without 'new'");
4  }
5
6  // ...
7};
  • 클래스 내부의 코드는 주변 코드가 아니더라도 항상 엄격 모드다.

4.2.2 인스턴스 속성 추가

1class Color {
2  constructor(r = 0, g = 0, b = 0) {
3    this.r = r;
4    this.g = g;
5    this.b = b;
6  }
7}
  • 인스턴스 속성은 기본 할당을 통해 생성되기 떄문에 구성, 쓰기, 열거가 가능하다.
  • 생성자 인수에서 가져오지 않는 속성 또한 당연히 설정할 수 있다.

4.2.3 프로로타입 메서드 추가

1class Color {
2  toString() {
3    return 'rgb(' + this.r + ', ' + this.g + ', ' + this.b + ')';
4  }
5}
6
7// 이전까지(~ES5)와 거의 동등한 코드
8Color.prototype.toString = function toString() {
9  return 'rgb(' + this.r + ', ' + this.g + ', ' + this.b + ')';
10};
  • ES2015의 메서드 구문은 더 선언적이고 간결하다.
  • 함수를 메서드로 구체적으로 표시하여 간단한 함수로는 가질 수 없는 기능에 접근할 수 있도록 한다. eg. super
  • 새로운 구문은 또한 메서드를 열거 불가능하게 만든다.
  • 메서드는 그 정의에 따라 생성자 함수가 아니므로 자바스크립트 엔진은 prototype 속성 및 관련 객체를 그 위에 배치하지 않는다.
1class Color {
2  toString() {
3    return "rgb(" + this.r + ", " + this.g + ", " + this.b + ")";
4  }
5}
6
7const c = new Color(30, 144, 255);
8console.log(typeof c.toString.prototype); // undefined
9
10...
11
12var Color = function Color(r, g, b) {};
13Color.prototype.toString = function toString() {}
14
15const c = new Color(30, 144, 255);
16console.log(typeof c.toString.prototype); // object
  • 이론적으로 메서드 구문은 이전 함수 구문보다 메모리 효율성이 높다.

4.2.4 정적 메서드 추가

  • static 키워드는 자바스크립트 엔진이 객체의 prototype이 아닌 객체 자체에 해당 메서드를 배치하도록 지시한다.
1const c = Color.fromCSS('#1E90FF');
2console.log(c.toString()); // "rgb(30, 144, 255)"
  • 이전 ES5에서는 Color 함수의 속성에 할당하여 작업을 수행했다.
1Color.fromCSS = function fromCSS(css) {};
  • 프로토타입 메서드와 마찬가지로 메서드 구문을 사용한다는 것은 fromCSS에 객체가 할당된 프로토타입 속성이 없다는 것을 의미한다.

4.2.5 접근자 속성 추가

1class Color {
2  get rgb() {
3    return 'rgb(' + this.r + ', ' + this.g + ', ' + this.b + ')';
4  }
5
6  toString() {
7    return this.rgb;
8  }
9}
  • ES5의 객체 리터럴에 접근자를 정의하는 것과 같다.
  • 한 가지 다른 차이점은 class 구문의 접근자 속성은 열거할 수 없다.
  • 정적 접근자 속성을 정의할 수도 있다. 앞에 static이 있는 접근자를 정의하기만 하면 된다.

4.2.6 계산된 메서드 이름

  • 경우에 따라 런타임에 결정되는 이름으로 메서드를 만들고 싶을 때가 있다.
    • 이는 심볼을 사용할 때 특히 중요하다.
1let name = 'foo' + Math.floor(Math.random() * 100);
2class SomeClass {
3  [name]() {}
4}
  • 정적 메서드 및 접근자 속성 메서드도 계산된 이름을 가질 수 있다.
1class SomeClass {}
  • 메서드 이름 주위의 대괄호에 유의하자. 속성 접근자에서와 똑같은 방식으로 작동한다.
    • 그 안에 어떤 표현이든 넣을 수 있다.
    • 클래스 정의가 평가 될 때 식이 평가된다.
    • 결과가 문자열이나 심볼이 아닌 경우 문자열로 변환된다.
    • 결과가 메서드 이름으로 사용된다.
  • 정적 메서드 및 접근자 속성 메서드도 계산된 이름을 가질 수 있다.
1class Guide {
2  static [6 * 7]() {
3    console.log('...');
4  }
5}
6
7Guide['42'](); // ...

4.3 기존 문법과 비교

  • ES2015 구문의 전체 클래스와 거의 동등한 ES5 버전과 비교해 보자.
1class Color {
2  constructor(r = 0, g = 0, b = 0) {
3    this.r = r;
4    this.g = g;
5    this.b = b;
6  }
7
8  get rgb() {
9    return 'rgb(' + this.r + ', ' + this.g + ', ' + this.b + ')';
10  }
11
12  set rgb(value) {
13    let s = String(value);
14    let match = /^rgb\((\d{1,3}),(\d{1,3}),(\d{1,3})\)$/i.exec(s.replace(/\s/g, ''));
15    if (!match) {
16      throw new Error("Invalid rgb color string '" + s + "'");
17    }
18    this.r = parseInt(match[1], 10);
19    this.g = parseInt(match[2], 10);
20    this.b = parseInt(match[3], 10);
21  }
22
23  toString() {
24    return this.rgb;
25  }
26
27  static fromCSS(css) {
28    const match = /^#?([0-9a-f]{3}|[0-9a-f]{6});?$/i.exec(css);
29    if (!match) {
30      throw new Error('Invalid CSS code: ' + css);
31    }
32    let vals = match[1];
33    if (vals.length === 3) {
34      vals = vals[0] + vals[0] + vals[1] + vals[1] + vals[2] + vals[2];
35    }
36    return new this(
37      parseInt(vals.substring(0, 2), 16),
38      parseInt(vals.substring(2, 4), 16),
39      parseInt(vals.substring(4, 6), 16),
40    );
41  }
42}
43
44// Usage
45let c = new Color(30, 144, 255);
46console.log(String(c)); // "rgb(30, 144, 255)"
47c = Color.fromCSS('00A');
48console.log(String(c)); // "rgb(0, 0, 170)"
1'use strict';
2var Color = function Color(r, g, b) {
3  if (!(this instanceof Color)) {
4    throw new TypeError("Class constructor Color cannot be invoked without 'new'");
5  }
6  this.r = r || 0;
7  this.g = g || 0;
8  this.b = b || 0;
9};
10
11Object.defineProperty(Color.prototype, 'rgb', {
12  get: function () {
13    return 'rgb(' + this.r + ', ' + this.g + ', ' + this.b + ')';
14  },
15  set: function (value) {
16    var s = String(value);
17    var match = /^rgb\((\d{1,3}),(\d{1,3}),(\d{1,3})\)$/i.exec(s.replace(/\s/g, ''));
18    if (!match) {
19      throw new Error("Invalid rgb color string '" + s + "'");
20    }
21    this.r = parseInt(match[1], 10);
22    this.g = parseInt(match[2], 10);
23    this.b = parseInt(match[3], 10);
24  },
25  configurable: true,
26});
27
28Color.prototype.toString = function () {
29  return this.rgb;
30};
31Color.fromCSS = function (css) {
32  var match = /^#?([0-9a-f]{3}|[0-9a-f]{6});?$/i.exec(css);
33  if (!match) {
34    throw new Error('Invalid CSS code: ' + css);
35  }
36  var vals = match[1];
37  if (vals.length === 3) {
38    vals = vals[0] + vals[0] + vals[1] + vals[1] + vals[2] + vals[2];
39  }
40  return new this(
41    parseInt(vals.substring(0, 2), 16),
42    parseInt(vals.substring(2, 4), 16),
43    parseInt(vals.substring(4, 6), 16),
44  );
45};
46
47// Usage
48var c = new Color(30, 144, 255);
49console.log(String(c)); // "rgb(30, 144, 255)"
50c = Color.fromCSS('00A');
51console.log(String(c)); // "rgb(0, 0, 170)"

4.4 서브클래스 만들기

  • ES5에서 생성자의 상속을 설정하는 것은 상당히 복잡하고 오류가 발생하기 쉽다.
  • 서브클래스에서 메서드의 "super" 버전을 사용하는 것은 훨씬 더 그렇다.
  • class 구문을 사용하면 모든 어려움이 사라진다.
1class Color {
2  constructor(r = 0, g = 0, b = 0) {
3    this.r = r;
4    this.g = g;
5    this.b = b;
6  }
7
8  get rgb() {
9    return 'rgb(' + this.r + ', ' + this.g + ', ' + this.b + ')';
10  }
11
12  toString() {
13    return this.rgb;
14  }
15}
16
17class ColorWithAlpha extends Color {}
18
19const c = new ColorWithAlpha(30, 144, 255);
20console.log(String(c)); // rgb(30, 144, 255)
  • 서브클래스의 기본 생성자는 여러 인수를 허용하고 모든 인수를 슈퍼클래스의 생성자를 전달한다.
1consturctor(/* ...여기에 임의의 수의 인수가 있다...  */){
2  super(/* ...모두 super로 전달... */);
3}
  • 서브클래스에 정의된 생성자가 없으면 자바스크립트 엔진이 기본 생성자를 제공한다.
    • 서브클래스가 생성자를 상속한다고 할 수도 있지만, 엔진은 슈퍼클래스의 생성자를 호출하는 별도의 함수를 생성한다.
  • ES5전의 코드는 아래와 같이 별도로 처리해주어야 할 게 많다.
1var ColorWithAlpha = function ColorWithAlpha() {
2  Color.apply(this, arguments);
3};
4
5ColorWithAlpha.prototype = Object.create(Color.prototype);
6ColorWithAlpha.prototype.constructor = ColorWithAlpha;
  • 오류가 발생할 가능성이 많은 상용구 코드이고, ColorWithAlpha에서 Color의 정적 속성과 메서드를 사용할 수 없다.

4.4.1 super 키워드

  • 클래스의 측면을 참조하기 위해 생성자와 메서드에서 super를 사용한다.
    • super(): 서브클래스 생성자에서 마치 객체를 생성하는 함수인 것처럼 super를 호출하고 슈퍼클래스가 객체의 초기화를 수행하도록 한다.
    • super.property와 super.method(): super.property와 super.method()가 대신 super에서 접근하여 슈퍼클래스 프로토타입의 속성 및 메서드를 참조한다.

4.4.2 서브클래스 생성자 작성

  • 서브클래스의 생성자에서는 객체가 생성된 후에만 사용할 수 있으며, super를 호출할 때까지 생성되지 않는다.
1class ColorWithAlpha extends Color {
2  constructor(r = 0, g = 0, b = 0, a = 0) {
3    this.a = a;
4    super(r, g, b);
5  }
6}
  • 위의 코드에서는 오류가 발생하는데, 이는 자바스크립트 엔진마다 다르다.
    • ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
    • ReferenceError: must call super constructor before using |this| in ColorWithAlpha class constructor
    • ReferenceError: this is not defined
  • 이 요구 사항은 생성되는 객체의 초기화가 기본에서 위로 수행되도록 하기 위한 것이다.

4.4.3 슈퍼클래스 프로토타입 속성, 메서드 상속, 접근

  • 때로는 서브클래스가 메서드의 정의를 재정의하여 슈퍼클래스에서 상속된 것을 사용하는 대신 자신의 것을 제공한다.
  • 때때로 서브클래스에서 메서드는 구현의 일부로 슈퍼클래스 메서드를 호출해야 하기도 한다.
    • 이를 위해 super.methodName() 을 통해 슈퍼클래스 메서드를 호출한다.
1class ColorWithAlpha extends Color {
2  constructor(r = 0, g = 0, b = 0, a = 1) {
3    super(r, g, b);
4    this.a = a;
5  }
6
7  brightness(bgColor) {
8    let result = super.brightness() * this.a;
9    if (bgColor && this.a !== 1) {
10      result = (result + bgColor.brightness() * (1 - this.a)) / 2;
11    }
12    return result;
13  }
14}
  • ES5전의 코드는 아래와 같이 처리할 수 있다.
1ColorWithAlpha.prototype.brightness = function brightness(bgColor) {
2  var result = Color.prototype.brightness.call(this) * this.a; // 또는 아래와 같이 선언 가능
3  if (bgColor && this.a !== 1) {
4    result = (result + bgColor.brightness() * (1 - this.a)) / 2;
5  }
6  return result;
7};
8
9// case1
10var superproto = Object.getPrototypeOf(ColorWithAlpha.prototype);
11var result = superproto.brightness.call(this) * this.a;
12
13// case2
14var superproto = Object.getPrototypeOf(Object.getPrototypeOf(this));
15var result = superproto.brightness.call(this) * this.a;
  • 이렇게 만든 코드는 어색하고 call을 사용해 this를 관리해야 하는 번거로움이 있다.

4.4.4 정적 메서드 상속

  • 자바스크립트에서 정적 메서드는 서브클래스에 상속된다.
1class Color {
2  // ...
3  static fromCSS() {
4    // ...
5  }
6}
7
8class ColorWithAlpha extends Color {
9  // ...
10}
11
12const ca = ColorWithAlpha.fromCSS('#1E90FF');
13console.log(String(ca)); // "rgba(30, 144, 255, 1)"
14console.log(ca.constructor.name); // "ColorWithAlpha"
15console.log(ca instanceof ColorWithAlpha); // true
  • extends 절을 사용하면 두 개의 상속 체인이 생성된다.
    • ColorWithAlphaColorFunction.PrototypeObject.prototype
    • ColorWithAlpha.prototypeColor.prototypeObject.prototype
  • 생성자 상속 체인은 ES2015의 새로운 기능이며 ES5 이전까지는 프로토타입이 Function.prototype 이 아닌 진정한 자바스크립트 함수를 갖는 표준 방법이 없었다.
  • 하지만 ES2015에서 서브클래스의 생성자가 슈퍼클래스 생성자를 프로토타입으로 사용하고 결과적으로 그 속성과 메서드를 상속하는 것은 완벽하다.

4.4.5 정적 메서드에서 super

  • 서브클래스 생성자는 실제로 슈퍼클래스 생성자에서 상속되므로 정적 서브클래스 메서드 내에서 super를 사용해 슈퍼클래스 버전 참조할 수 있다.
1class ColorWithAlpha extends Color {
2  // ...
3
4  static fromCSS(css, a = 1) {
5    const result = super.fromCSS(css);
6    result.a = a;
7    return result;
8  }
9}

4.4.6 새 인스턴스를 반환하는 메서드

  • 인스턴스 메서드에서는 this.constructor가 일반적으로 객체의 생성자를 참조하므로 this가 아니라 this.constructor를 사용할 수 있다.
1class Color {
2  halfBright() {
3    const ctor = this.constructor || Color;
4    return new ctor(Math.round(this.r / 2), Math.round(this.g / 2), Math.round(this.b / 2));
5  }
6}
  • 위 예제에서 halfBright가 Color 인스턴스를 반환하는 대신 Color 서브클래스를 만들고 싶다고 가정해 보겠다.
  • 새 인스턴스를 만드는 대부분의 메서드가 서브클래스에서 쉽게 재정의할 수 잇는 방식으로 동일한 생성자를 사용하도록 하려면 Symbol.species라는 더 나은 대안이 있다.
  • Symbol.species는 “그” 클래스의 새 인스턴스를 만들어야 하는 메서드에서 발생하는 작업을 서브클래스가 제어할 수 있도록 특별히 설계된 패턴의 일부다.
1class Base {
2  constructor(data) {
3    this.data = data;
4  }
5
6  static get [Symbol.species]() {
7    return this;
8  }
9
10  static create(data) {
11    // Symbol.species를 사용하지 않는다.
12    const ctor = this || Base;
13    return new ctor(data);
14  }
15
16  clone() {
17    // Symbol.species를 사용한다.
18    const ctor = (this && this.constructor && this.constructor[Symbol.species]) || Base;
19    return new ctor(this.data);
20  }
21}
22
23// Sub1은 기본 동작을 사용한다.
24class Sub1 extends Base {}
25
26// Sub2는 패턴을 따르는 모든 메서드가 Sub2 대신 Base를 사용하게 한다.
27class Sub2 extends Base {
28  static get [Symbol.species]() {
29    return Base;
30  }
31}
32
33const a = Base.create(1);
34console.log(a.constructor.name); // "Base"
35const aclone = a.clone();
36console.log(aclone.constructor.name); // "Base"
37
38const b = Sub1.create(2);
39console.log(b.constructor.name); // "Sub1"
40const bclone = b.clone();
41console.log(bclone.constructor.name); // "Sub1"
42
43const c = Sub2.create(3);
44console.log(c.constructor.name); // "Sub2"
45const d = new Sub2(4);
46console.log(d.constructor.name); // "Sub2"
47console.log(d.data); // 4
48const dclone = d.clone();
49console.log(dclone.constructor.name); // "Base"
50console.log(dclone.data); // 4

4.4.7 내장 객체 상속

  • ES5에서 Error와 Array 같은 일부 내장 생성자는 서브클래스를 제대로 만들지 못하는 것으로 악명이 높다.
    • 이는 ES2015에서 수정되었다.
  • 내장 객체를 class로 서브클래싱 하는 것은 다른 것을 서브클래싱하는 것과 같다.
    • Reflect.construct를 통해 class 구문 없이 서브클래싱할 수도 있다.
1class Elements extends Array {
2  select(source) {
3    // ...
4    return this;
5  }
6
7  style(props) {
8    // ...
9    return this;
10  }
11}
12
13new Elements().select('div').style({ color: 'green' }).slice(1).style({ border: '1px solid gray' });
  • 슬라이스를 사용해 새 Array가 아닌 새 Elements 인스턴스가 만들어지는 점에 유의하자.
  • Array는 Symbol.species 패턴을 사용하고 Elements 클래스가 기본값을 재정의하지 않았기 때문이다.

4.4.8 super를 이용할 수 있는 곳

  • ES2015 이전에는 자바스크립트에서 메서드라는 용어가 비교적 느슨하게 사용하여 객체 속성에 할당된 모든 함수를 참조했다.
  • ES2015에서는 실제 메서드와 속성에 할당된 함수 사이에는 차이가 있다.
    • 실제 메서드 내부의 코드에서만 모두 super에 접근이 가능하다.
1class SuperClass {
2  test() {
3    return "SuperClass's test";
4  }
5}
6class SubClass extends SuperClass {
7  test1() {
8    return "SubClass's test1: " + super.test();
9  }
10}
11SubClass.prototype.test2 = function () {
12  return "SubClass's test2: " + super.test(); // ERROR HERE
13};
14
15const obj = new SubClass();
16obj.test1();
17obj.test2();
  • 메서드는 생성된 객체에 대한 링크가 있지만 속성에 할당된 기존 함수에는 링크가 없기 때문이다.
1function getFakeSuper(o) {
2  return Object.getPrototypeOf(Object.getPrototypeOf(o));
3}
4class Base {
5  test() {
6    console.log("Base's test");
7    return 'Base test';
8  }
9}
10class Sub extends Base {
11  test() {
12    console.log("Sub's test");
13    return 'Sub test > ' + getFakeSuper(this).test.call(this);
14  }
15}
16class SubSub extends Sub {
17  test() {
18    console.log("SubSub's test");
19    return 'SubSub test > ' + getFakeSuper(this).test.call(this);
20  }
21}
22
23const obj = new SubSub();
24console.log(obj.test()); // "SubSub's test" 후 "Sub's test" 반복
25// stack overflow error가 발생할 때까지
  • 위의 코드가 메서드가 정의된 객체([[HomeObject]])를 나타내는 필드를 가져야하는 이유다.
  • 자바스크립트 엔진은 해당 객체를 얻은 후에야 프로토타입을 가져와 super에 접근할 수 있다.
  • 객체에서 객체로 메서드를 복사하는 것(mixin 패턴)은 메서드의 [[HomeObject]]를 변경하지 않는다. mixin 메서드에서 super를 사용하는 경우 복사된 객체의 프로토타입이 아닌 원래 HomeObject의 프로토타입을 사용한다.
    • 이는 복사한 대상 간에 미묘한 혼선이 발생할 수 있으니 주의하자.

4.5 Object.protoype 떠나보내기

  • 기본적으로 기본 클래스도 사실상 Object의 서브클래스이다.
  • 아래 코드의 다음 두 클래스는 사실상 동일하다.
    1class A {
    2  constructor() {}
    3}
    4class B extends Object {
    5  constructor() {
    6    super();
    7  }
    8}
  • 다만 A함수의 프로토타입은 Function.prototype이지만, B 함수의 프로토타입은 Object이다.
  • A.prototype과 B.prototype은 모두 Object.prototype이다.

4.6 new.target

  • 함수와 생성자는 두 가지 방법으로 호출할 수 있다.
    1. 직접 호출(클래스 구문을 통해 생성된 생성자는 이를 허용하지 않음)
    2. 객체 생성의 일부로서 호출(super 또는 Reflect.construct를 통해서)
  • 때로는 함수가 어떻게 호출되었는지 아는 것이 중요하다.
    • 함수가 new 를 통하지 않고 직접 호출된 경우 new.target 은 undefined , new 연산자의 직접 대상인 경우엔 현재 함수를 참조
1function example() {
2  console.log(new.target);
3}
4
5example(); // undefined
6
7class Base {
8  constructor() {
9    console.log(new.target);
10  }
11}
12
13new Base(); // "Base"
14
15class Sub extends Base {
16  constructor() {
17    super();
18  }
19}
20
21new Sub(); // "Sub"
  • 이 성질을 이용해 다음 세 가지 시나리오에 대해 적절한 처리가 가능
  • 추상 클래스
    • 직접 인스턴스화 할 수 없는 클래스로 서브클래스를 통해서만 인스턴스화 가능
    • 생성자에서 new.target 이 자기 자신을 바라보면 오류를 발생시켜 구현
1class Shape {
2  constructor(color) {
3    if (new.target === Shape) {
4      throw new Error("Shape can't be directly instantiated");
5    }
6    this.color = color;
7  }
8  // ...
9}
10
11class Triangle extends Shape {
12  get sides() {
13    return 3;
14  }
15}
16
17const t = new Triangle('orange');
18
19const s = new Shape('red'); // Error: "Shape can't be directly instantiated"
  • 최종 클래스
    • 최종 클래스는 서브클래스 인스턴스를 허용하지 않음
    • 생성자에서 new.target 이 클래스의 자체 생성자와 같지 않으면 오류를 발생시켜 구현
1class Thingy {
2  constructor() {
3    if (new.target !== Thingy) {
4      throw new Error("Thingy subclasses aren't supported.");
5    }
6  }
7}
8
9class InvalidThingy extends Thingy {}
10
11const can = new Thingy(); // works
12const cannot = new InvalidThingy(); // Error: "Thingy subclasses aren't supported."
  • 생성자 호출/함수 호출에 따른 각각 다른 처리
1const TwoWays = function TwoWays() {
2  if (!new.target) {
3    console.log("Called directly; using 'new' instead");
4    return new TwoWays();
5  }
6  console.log("Called via 'new'");
7};
8console.log('With new:');
9let t1 = new TwoWays();
10// "Called via 'new'"
11
12console.log('Without new:');
13let t2 = TwoWays();
14// "Called directly; using 'new' instead"
15// "Called via 'new'"

4.7 클래스 선언 대 클래스 표현식

  • function과 마찬가지로 class는 선언이나 표현식으로 사용할 수 있다.
1// 선언
2class Class1 {}
3
4// 익명 클래스 표현식
5let Color = class {};
6
7// 명명된 클래스 표현식
8let C = class Color {};

4.7.1 class 선언

  • class 선언은 몇몇 중요한 차이점이 있지만 function 선언과 매우 유사하게 작동한다.
  • function 선언과 똑같이 동작하는 것
    • 현재 범위에 클래스 이름을 추가한다.
    • 닫는 중괄호 뒤에 세미콜론이 필요하지 않다.
  • function 선언과 다르게 동작하는 것
    • 호이스트되지 않고 절반만 호이스트된다.
    • 식별자는 범위 전체에서 예약되지만 코드의 단계 별 실행에서 선언에 도달할 때까지 초기화되지 않는다.
    • 임시 데드존에 참여한다.
    • 전역 스코프에서 사용되는 경우 클래스 이름에 대한 전역 객체에 속성을 만들지 말라. 대신 전역 객체의 속성이 아닌 전역을 만든다.
1// 여기서 TheClass를 사용하려고하면 TDZ때문에 ReferenceError가 발생한다.
2
3let name = 'foo' + Math.floor(Math.random() * 1000);
4
5class TheClass {
6  // 선언은 단계별 코드의 일부로 처리되기 때문에
7  // 여기에서 name을 사용할 수 있고 위에서 할당한 값을 가진다.
8  [name]() {
9    console.log('This is the method ' + name);
10  }
11} // 세미콜론 필요 없음
12
13// 전역이 생성됨
14console.log(typeof TheClass); // "function"
15
16// 전역 객체애 대한 속성이 없음
17console.log(typeof this.TheClass); // "undefined"

4.7.2 class 표현

  • class 표현식은 function 표현식과 매우 유사하게 작동한다.
    • 명명된 방식과 익명 방식이 모두 있다.
    • 클래스 이름이 나타나는 범위에 클래스 이름을 추가하지 않지만 클래스 정의 자체 내에서 클래스 이름을 사용할 수 있도록 한다.
    • 변수나 상수에 할당되거나 함수에 전달되거나 무시될 수 있는 값(클래스의 생성자)이 생성된다.
    • 자바스크립트 엔진은 익명 함수식에 대해 컨텍스트에서 익명 클래스 식으로 만든 클래스의 name 속성 값을 유추한다.
    • 할당의 오른쪽으로 사용되는 경우 할당식을 종료하지 않는다.
1let name = 'foo' + Math.floor(Math.random() * 1000);
2
3// 표현식
4const C = class TheClass {
5  [name]() {
6    console.log('This is the method ' + name + ' in the class ' + TheClass.name);
7    // The class name is in-scope -^
8    // within the definition
9  }
10}; // 세미콜론 필요함
11
12// 클래스 이름이 이 스코프 영역에 추가되지 않음
13console.log(typeof TheClass); // "undefined"
14
15// 표현식의 값은 클래스임
16console.log(typeof C); // "function"

4.8 앞으로 더 배울 것

  • 공용 클래스 속성
  • 프라이빗 필드
  • 프라이빗 메서드
  • 퍼블릭과 프라이빗 정적 필드
  • class 정의(예: ES2021)에 추가될 몇 가지 기능

4.9 과거 습관을 새롭게

4.9.1 생성자 함수를 만들 때 클래스 사용

  • 생성자 함수를 사용하지 않는다면 클래스 사용을 시작해야 한다는 의미는 아니다.
  • 생성자 함수와 prototype 속성은 자바스크립트의 프로토타입 상속을 사용하는 방법 중 하나일뿐이다.
  • 생성자 함수를 사용한다면 class를 사용하는게 간결함, 표현력 기능의 모든 이점을 고려할 때 유익하다.