9. 클래스(1)

protect-me·2021년 6월 19일
0
post-thumbnail

9.1 클래스와 기본 문법


요약

class MyClass {
  prop = value; // 프로퍼티 (필드)
  //
  constructor(...) { // 생성자 메서드
    // ...
  }
  method(...) {} // 메서드
  //
  get something(...) {} // getter 메서드
  set something(...) {} // setter 메서드
  //
  [Symbol.iterator]() {} // 계산된 이름(computed name)을 사용해 만드는 메서드 (심볼)
  // ...
}

MyClassconstructor의 코드를 본문으로 갖는 함수입니다. MyClass에서 정의한 일반 메서드나 getter, setterMyClass.prototype에 쓰입니다.

클래스는 객체 지향 프로그래밍에서 특정 객체를 생성하기 위해 변수와 메소드를 정의하는 일종의 틀로, 객체를 정의하기 위한 상태(멤버 변수)와 메서드(함수)로 구성된다.

  • 실무에선 사용자나 물건같이 동일한 종류의 객체를 여러 개 생성해야 하는 경우가 잦습니다. 이럴 때 'new' 연산자와 생성자 함수에서 배운 new function을 사용할 수 있습니다.
  • 여기에 더하여 모던 자바스크립트에 도입된 클래스(class)라는 문법을 사용하면 객체 지향 프로그래밍에서 사용되는 다양한 기능을 자바스크립트에서도 사용할 수 있습니다.

기본 문법

class MyClass {
  // 여러 메서드를 정의할 수 있음
  constructor() { ... }
  method1() { ... }
  method2() { ... }
  method3() { ... }
  ...
}
  • 이렇게 클래스를 만들고, new MyClass()를 호출하면 내부에서 정의한 메서드가 들어 있는 객체가 생성됩니다.
  • 객체의 기본 상태를 설정해주는 생성자 메서드 constructor()new에 의해 자동으로 호출되므로, 특별한 절차 없이 객체를 초기화 할 수 있습니다.
class User {
  constructor(name) {
    this.name = name;
  }
  sayHi() {
    alert(this.name);
  }
}

// 사용법:
let user = new User("John");
user.sayHi();
  1. 새로운 객체가 생성됩니다.
  2. 넘겨받은 인수와 함께 constructor가 자동으로 실행됩니다. 이때 인수 "John"this.name에 할당됩니다

메서드 사이엔 쉼표가 없습니다.

클래스란

  • 자바스크립트에서 클래스는 함수의 한 종류입니다.
class User {
  constructor(name) { this.name = name; }
  sayHi() { alert(this.name); }
}
// User가 함수라는 증거
alert(typeof User); // function
  • class User {...} 문법 구조가 진짜 하는 일은 다음과 같습니다.
  1. User라는 이름을 가진 함수를 만듭니다. 함수 본문은 생성자 메서드 constructor에서 가져옵니다. 생성자 메서드가 없으면 본문이 비워진 채로 함수가 만들어집니다.
  2. sayHi같은 클래스 내에서 정의한 메서드를 User.prototype에 저장합니다.
  • new User를 호출해 객체를 만들고, 객체의 메서드를 호출하면 함수의 prototype 프로퍼티에서 설명한 것처럼 메서드를 프로토타입에서 가져옵니다. 이 과정이 있기 때문에 객체에서 클래스 메서드에 접근할 수 있습니다.

class User {
  constructor(name) { this.name = name; }
  sayHi() { alert(this.name); }
}

// 클래스는 함수입니다.
alert(typeof User); // function

// 정확히는 생성자 메서드와 동일합니다.
alert(User === User.prototype.constructor); // true

// 클래스 내부에서 정의한 메서드는 User.prototype에 저장됩니다.
alert(User.prototype.sayHi); // alert(this.name);

// 현재 프로토타입에는 메서드가 두 개입니다.
alert(Object.getOwnPropertyNames(User.prototype)); // constructor, sayHi

클래스는 단순한 편의 문법이 아닙니다

// class User와 동일한 기능을 하는 순수 함수를 만들어보겠습니다.

// 1. 생성자 함수를 만듭니다.
function User(name) {
  this.name = name;
}
// 모든 함수의 프로토타입은 'constructor' 프로퍼티를 기본으로 갖고 있기 때문에
// constructor 프로퍼티를 명시적으로 만들 필요가 없습니다.

// 2. prototype에 메서드를 추가합니다.
User.prototype.sayHi = function() {
  alert(this.name);
};

// 사용법:
let user = new User("John");
user.sayHi();

차이점

  1. class로 만든 함수엔 특수 내부 프로퍼티인 [[FunctionKind]]:"classConstructor"가 이름표처럼 붙습니다. 이것만으로도 두 방법엔 분명한 차이가 있음을 알 수 있습니다.
    • 자바스크립트는 다양한 방법을 사용해 함수에 [[FunctionKind]]:"classConstructor가 있는지를 확인합니다. 이런 검증 과정이 있기 때문에 클래스 생성자를 new와 함께 호출하지 않으면 에러가 발생합니다.
    • 대부분의 자바스크립트 엔진이 클래스 생성자를 문자열로 표현할 때 'class…'로 시작하는 문자열로 표현한다는 점 역시 다릅니다.
  2. 클래스 메서드는 열거할 수 없습니다(non-enumerable). 클래스의 prototype 프로퍼티에 추가된 메서드 전체의 enumerable 플래그는 false입니다.
    for..in으로 객체를 순회할 때, 메서드는 순회 대상에서 제외하고자 하는 경우가 많으므로 이 특징은 꽤 유용합니다.
  3. 클래스는 항상 엄격 모드로 실행됩니다(use strict). 클래스 생성자 안 코드 전체엔 자동으로 엄격 모드가 적용됩니다.

클래스 표현식

  • 함수처럼 클래스도 다른 표현식 내부에서 정의, 전달, 반환, 할당할 수 있습니다.
let User = class {
  sayHi() {
    alert("Hello");
  }
};
  • 기명 함수 표현식(Named Function Expression)과 유사하게 클래스 표현식에도 이름을 붙일 수 있습니다.
// 기명 클래스 표현식(Named Class Expression)
// (명세서엔 없는 용어이지만, 기명 함수 표현식과 유사하게 동작합니다.)
let User = class MyClass {
  sayHi() {
    alert(MyClass); // MyClass라는 이름은 오직 클래스 안에서만 사용할 수 있습니다.
  }
};

new User().sayHi(); // 제대로 동작합니다(MyClass의 정의를 보여줌).
alert(MyClass); // ReferenceError: MyClass is not defined, MyClass는 클래스 밖에서 사용할 수 없습니다.
  • 아래와 같이 ‘필요에 따라’ 클래스를 동적으로 생성하는 것도 가능합니다.
function makeClass(phrase) {
  // 클래스를 선언하고 이를 반환함
  return class {
    sayHi() {
      alert(phrase);
    };
  };
}

// 새로운 클래스를 만듦
let User = makeClass("Hello");
new User().sayHi(); // Hello

getter와 setter

class User {

  constructor(name) {
    // setter를 활성화합니다.
    this.name = name;
  }

  get name() {
    return this._name;
  }

  set name(value) {
    if (value.length < 4) {
      alert("이름이 너무 짧습니다.");
      return;
    }
    this._name = value;
  }

}

let user = new User("John");
alert(user.name); // John

user = new User(""); // 이름이 너무 짧습니다.

계산된 메서드 이름 […]

  • 대괄호 [...]를 이용해 계산된 메서드 이름(computed method name)을 만드는 예시를 살펴봅시다.
class User {
  ['say' + 'Hi']() {
    alert("Hello");
  }
}
new User().sayHi();

클래스 필드

  • '클래스 필드(class field)'라는 문법을 사용하면 어떤 종류의 프로퍼티도 클래스에 추가할 수 있습니다.
class User {
  name = "John";

  sayHi() {
    alert(`Hello, ${this.name}!`);
  }
}

new User().sayHi(); // Hello, John!
  • 클래스를 정의할 때 '<프로퍼티 이름> = <값>'을 써주면 간단히 클래스 필드를 만들 수 있습니다.
  • 클래스 필드의 중요한 특징 중 하나는 User.prototype이 아닌 개별 객체에만 클래스 필드가 설정된다는 점입니다.
class User {
  name = "John";
}

let user = new User();
alert(user.name); // John
alert(User.prototype.name); // undefined
  • 아울러 클래스 필드엔 복잡한 표현식이나 함수 호출 결과를 사용할 수 있습니다.
class User {
  name = prompt("이름을 알려주세요.", "보라");
}

let user = new User();
alert(user.name); // 보라

클래스 필드로 바인딩 된 메서드 만들기

함수 바인딩 챕터에서 살펴본 것처럼 자바스크립트의 함수는 동적인 this를 갖습니다.
따라서 객체 메서드를 여기저기 전달해 전혀 다른 컨텍스트에서 호출하게 되면 this는 원래 객체를 참조하지 않습니다.

class Button {
  constructor(value) {
    this.value = value;
  }

  click() {
    alert(this.value);
  }
}

let button = new Button("hello");

setTimeout(button.click, 1000); // undefined
  • 이렇게 된 이유는 setTimeout에 객체에서 분리된 함수인 button.click이 전달되기 때문입니다.
  • 이렇게 this의 컨텍스트를 알 수 없게 되는 문제를 '잃어버린 this(losing this)'라고 합니다.
  • 문제를 해결하기 위해 두 개의 방법을 사용할 수 있는데 함수 바인딩에서 이 방법에 대해 살펴본 바 있습니다.
  1. setTimeout(() => button.click(), 1000) 같이 래퍼 함수를 전달하기
    (외부 렉시컬 환경에서 click을 받아서 보통 때처럼 메서드를 호출했기 때문에 에러가 발생하지 않음)
  2. 생성자 안 등에서 메서드를 객체에 바인딩하기
  3. 클래스 필드에서 제공하는 또 다른 방법
class Button {
  constructor(value) {
    this.value = value;
  }
  click = () => {
    alert(this.value);
  }
}

let button = new Button("hello");

setTimeout(button.click, 1000); // hello
  • 클래스 필드 click = () => {...}는 각 Button 객체마다 독립적인 함수를 만들고 함수의 this를 해당 객체에 바인딩시켜줍니다. 따라서 개발자는 button.click을 아무 곳에나 전달할 수 있고, this엔 항상 의도한 값이 들어가게 됩니다.
  • 클래스 필드의 이런 기능은 브라우저 환경에서 메서드를 이벤트 리스너로 설정해야 할 때 특히 유용합니다.


9.2 클래스 상속


요약
1. 클래스 확장하기: class Child extends Parent

  • Child.prototype.__proto__Parent.prototype이 되므로 메서드 전체가 상속됩니다.
  1. 생성자 오버라이딩:
    this를 사용하기 전에 Child 생성자 안에서 super()로 부모 생성자를 반드시 호출해야 합니다.
  2. 메서드 오버라이딩:
    Child에 정의된 메서드에서 super.method()를 사용해 Parent에 정의된 메서드를 사용할 수 있습니다.
  3. super 키워드와 [[HomeObject]]
    메서드는 내부 프로퍼티 [[HomeObject]]에 자신이 정의된 클래스와 객체를 기억해놓습니다. super[[HomeObject]]를 사용해 부모 메서드를 찾습니다.
    따라서 super가 있는 메서드는 객체 간 복사 시 제대로 동작하지 않을 수 있습니다.
    추가 사항:
    화살표 함수는 thissuper를 갖지 않으므로 주변 컨텍스트에 잘 들어맞습니다.
  • 클래스 상속을 사용하면 클래스를 다른 클래스로 확장할 수 있습니다. 기존에 존재하던 기능을 토대로 새로운 기능을 만들 수 있죠.

‘extends’ 키워드

class Animal {
  constructor(name) {
    this.speed = 0;
    this.name = name;
  }
  run(speed) {
    this.speed = speed;
    alert(`${this.name} 은/는 속도 ${this.speed}로 달립니다.`);
  }
  stop() {
    this.speed = 0;
    alert(`${this.name} 이/가 멈췄습니다.`);
  }
}

let animal = new Animal("동물");

  • Animal을 상속받는 class Rabbit
class Rabbit extends Animal {
  hide() {
    alert(`${this.name} 이/가 숨었습니다!`);
  }
}

let rabbit = new Rabbit("흰 토끼");

rabbit.run(5); // 흰 토끼 은/는 속도 5로 달립니다.
rabbit.hide(); // 흰 토끼 이/가 숨었습니다!
  • 클래스 Rabbit을 사용해 만든 객체는 rabbit.hide() 같은 Rabbit에 정의된 메서드에도 접근할 수 있고, rabbit.run() 같은 Animal에 정의된 메서드에도 접근할 수 있습니다.
  • 키워드 extends는 프로토타입을 기반으로 동작합니다. extendsRabbit.prototype.[[Prototype]]Animal.prototype으로 설정합니다. 그렇기 때문에 Rabbit.prototype에서 메서드를 찾지 못하면 Animal.prototype에서 메서드를 가져옵니다.

  1. 객체 rabbitrun이 있나 확인합니다. run이 없네요.
  2. rabbit의 프로토타입인 Rabbit.prototype에 메서드가 있나 확인합니다. hide는 있는데 run은 없습니다.
  3. extends를 통해 관계가 만들어진 Rabbit.prototype의 프로토타입, Animal.prototype에 메서드가 있나 확인합니다. 드디어 메서드 run을 찾았습니다.
  • 네이티브 프로토타입에서 알아본 것처럼 자바스크립트의 내장 객체는 프로토타입을 기반으로 상속 관계를 맺습니다. Date.prototype.[[Prototype]]Object.prototype인 것처럼 말이죠. Date 객체에서 일반 객체 메서드를 사용할 수 있는 이유가 바로 여기에 있습니다.

extends 뒤에 표현식이 올 수도 있습니다.

  • 여기서 class Userf("Hello")의 반환 값을 상속받습니다.
function f(phrase) {
  return class {
    sayHi() { alert(phrase) }
  }
}
class User extends f("Hello") {}
new User().sayHi(); // Hello

메서드 오버라이딩

  • 이제 한발 더 나아가 메서드를 오버라이딩 해봅시다. 특별한 사항이 없으면 class Rabbitclass Animal에 있는 메서드를 ‘그대로’ 상속받습니다.
  • 그런데 Rabbit에서 stop() 등의 메서드를 자체적으로 정의하면, 상속받은 메서드가 아닌 자체 메서드가 사용됩니다.
class Rabbit extends Animal {
  stop() {
    // rabbit.stop()을 호출할 때
    // Animal의 stop()이 아닌, 이 메서드가 사용됩니다.
  }
}
  • 개발을 하다 보면 부모 메서드 전체를 교체하지 않고, 부모 메서드를 토대로 일부 기능만 변경하고 싶을 때가 생깁니다. 부모 메서드의 기능을 확장하고 싶을 때도 있죠. 이럴 때 커스텀 메서드를 만들어 작업하게 되는데, 이미 커스텀 메서드를 만들었더라도 이 과정 전·후에 부모 메서드를 호출하고 싶을 때가 있습니다. 키워드 super는 이럴 때 사용합니다.
  • super.method(...)는 부모 클래스에 정의된 메서드, method를 호출합니다.
  • super(...)는 부모 생성자를 호출하는데, 자식 생성자 내부에서만 사용 할 수 있습니다.
class Animal {

  constructor(name) {
    this.speed = 0;
    this.name = name;
  }

  run(speed) {
    this.speed = speed;
    alert(`${this.name}가 속도 ${this.speed}로 달립니다.`);
  }

  stop() {
    this.speed = 0;
    alert(`${this.name}가 멈췄습니다.`);
  }

}

class Rabbit extends Animal {
  hide() {
    alert(`${this.name}가 숨었습니다!`);
  }

  stop() {
    super.stop(); // 부모 클래스의 stop을 호출해 멈추고,
    this.hide(); // 숨습니다.
  }
}

let rabbit = new Rabbit("흰 토끼");

rabbit.run(5); // 흰 토끼가 속도 5로 달립니다.
rabbit.stop(); // 흰 토끼가 멈췄습니다. 흰 토끼가 숨었습니다!
  • Rabbit은 이제 실행 중간에 부모 클래스에 정의된 메서드 super.stop()을 호출하는 stop을 가지게 되었네요.

화살표 함수엔 super가 없습니다.

  • super에 접근하면 아래 예시와 같이 super를 외부 함수에서 가져옵니다.
class Rabbit extends Animal {
  stop() {
    setTimeout(() => super.stop(), 1000); // 1초 후에 부모 stop을 호출합니다.
  }
}
  • 화살표 함수의 superstop()super와 같아서 위 예시는 의도한 대로 동작합니다. 그렇지만 setTimeout안에서 ‘일반’ 함수를 사용했다면 에러가 발생했을 겁니다.
// Unexpected super
setTimeout(function() { super.stop() }, 1000);

생성자 오버라이딩

  • 명세서에 따르면, 클래스가 다른 클래스를 상속받고 constructor가 없는 경우엔 아래처럼 ‘비어있는’ constructor가 만들어집니다.
class Rabbit extends Animal {
  // 자체 생성자가 없는 클래스를 상속받으면 자동으로 만들어짐
  constructor(...args) {
    super(...args);
  }
}
  • 보시다시피 생성자는 기본적으로 부모 constructor를 호출합니다. 이때 부모 constructor에도 인수를 모두 전달합니다. 클래스에 자체 생성자가 없는 경우엔 이런 일이 모두 자동으로 일어납니다.
class Animal {
  constructor(name) {
    this.speed = 0;
    this.name = name;
  }
  // ...
}

class Rabbit extends Animal {
  constructor(name, earLength) {
    this.speed = 0;
    this.name = name;
    this.earLength = earLength;
  }

  // ...
}

// 동작하지 않습니다!
let rabbit = new Rabbit("흰 토끼", 10); 
// ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
  • 상속 클래스의 생성자에선 반드시 super(...)를 호출해야 하는데, super(...)를 호출하지 않아 에러가 발생했습니다. super(...)this를 사용하기 전에 반드시 호출해야 합니다.
  • 그런데 왜 super(...)를 호출해야 하는 걸까요? 당연히 이유가 있습니다. 상속 클래스의 생성자가 호출될 때 어떤 일이 일어나는지 알아보며 이유를 찾아봅시다.
  • 자바스크립트는 '상속 클래스의 생성자 함수(derived constructor)'와 그렇지 않은 생성자 함수를 구분합니다. 상속 클래스의 생성자 함수엔 특수 내부 프로퍼티인 [[ConstructorKind]]:"derived"가 이름표처럼 붙습니다.
  • 일반 클래스의 생성자 함수와 상속 클래스의 생성자 함수 간 차이는 new와 함께 드러납니다.
    - 일반 클래스가 new와 함께 실행되면, 빈 객체가 만들어지고 this에 이 객체를 할당합니다.
    - 반면, 상속 클래스의 생성자 함수가 실행되면, 일반 클래스에서 일어난 일이 일어나지 않습니다. 상속 클래스의 생성자 함수는 빈 객체를 만들고 this에 이 객체를 할당하는 일을 부모 클래스의 생성자가 처리해주길 기대합니다.
  • 이런 차이 때문에 상속 클래스의 생성자에선 super를 호출해 부모 생성자를 실행해 주어야 합니다. 그렇지 않으면 this가 될 객체가 만들어지지 않아 에러가 발생합니다.
class Animal {

  constructor(name) {
    this.speed = 0;
    this.name = name;
  }

  // ...
}

class Rabbit extends Animal {

  constructor(name, earLength) {
    super(name);
    this.earLength = earLength;
  }

  // ...
}

// 이제 에러 없이 동작합니다.
let rabbit = new Rabbit("흰 토끼", 10);
alert(rabbit.name); // 흰 토끼
alert(rabbit.earLength); // 10

클래스 필드 오버라이딩: 까다로운 내용

class Animal {
  name = 'animal'

  constructor() {
    alert(this.name); // (*)
  }
}

class Rabbit extends Animal {
  name = 'rabbit';
}

new Animal(); // animal
new Rabbit(); // animal
  • Animal을 상속받는 Rabbit에서 name 필드를 오버라이딩 했습니다. Rabbit에는 따로 생성자가 정의되어 있지 않기 때문에 Rabbit을 사용해 인스턴스를 만들면 Animal의 생성자가 호출됩니다.
  • 흥미로운 점은 new Animal()new Rabbit()을 실행할 때 두 경우 모두 (*)로 표시한 줄에 있는 alert 함수가 실행되면서 얼럿 창에 animal이 출력된다는 점입니다.
  • 이를 통해 우리는 ‘부모 생성자는 자식 클래스에서 오버라이딩한 값이 아닌, 부모 클래스 안의 필드 값을 사용한다’ 는 사실을 알 수 있습니다.
  • 상속을 받고 필드 값을 오버라이딩했는데 새로운 값 대신 부모 클래스 안에 있는 기존 필드 값을 사용하다니 이상하지 않나요? 이해를 돕기 위해 이 상황을 메서드와 비교해 생각해봅시다.
class Animal {
  showName() {  // this.name = 'animal' 대신 메서드 사용
    alert('animal');
  }

  constructor() {
    this.showName(); // alert(this.name); 대신 메서드 호출
  }
}

class Rabbit extends Animal {
  showName() {
    alert('rabbit');
  }
}

new Animal(); // animal
new Rabbit(); // rabbit
  • 필드를 오버라이딩한 위쪽 예시와 결과가 다르네요. 위와 같이 자식 클래스에서 부모 생성자를 호출하면 오버라이딩한 메서드가 실행되어야 합니다. 이게 우리가 원하던 결과죠.
  • 그런데 클래스 필드는 자식 클래스에서 필드를 오버라이딩해도 부모 생성자가 오버라이딩한 필드 값을 사용하지 않습니다. 부모 생성자는 항상 부모 클래스에 있는 필드의 값을 사용합니다. 왜 이런 차이가 있을까요?
  • 이유는 필드 초기화 순서 때문입니다. 클래스 필드는 다음과 같은 규칙에 따라 초기화 순서가 달라집니다.
    - 아무것도 상속받지 않는 베이스 클래스는 생성자 실행 이전에 초기화됨
    - 부모 클래스가 있는 경우엔 super() 실행 직후에 초기화됨
  • 위 예시에서 Rabbit은 하위 클래스이고 constructor()가 정의되어 있지 않습니다. 이런 경우 앞서 설명한 바와 같이 생성자는 비어있는데 그 안에 super(...args)만 있다고 보면 됩니다.
  • 따라서 new Rabbit()을 실행하면 super()가 호출되고 그 결과 부모 생성자가 실행됩니다. 그런데 이때 하위 클래스 필드 초기화 순서에 따라 하위 클래스 Rabbit의 필드는 super() 실행 후에 초기화됩니다. 부모 생성자가 실행되는 시점에 Rabbit의 필드는 아직 존재하지 않죠. 이런 이유로 필드를 오버라이딩 했을 때 Animal에 있는 필드가 사용된 것입니다.
  • 이렇게 자바스크립트는 오버라이딩시 필드와 메서드의 동작 방식이 미묘하게 다릅니다.
  • 개발하다가 필드 오버라이딩이 문제가 되는 상황이 발생하면 필드 대신 메서드를 사용하거나 gettersetter를 사용해 해결하면 됩니다.

정리

  • 필드의 경우, 하위 class에서 constructor가 없을 때, 상위 classconstructor를 먼저 찾아가서 실행하고 나서 하위 class의 필드를 초기화함. 따라서 상위 class의 필드를 사용하게 됨.
  • 메소드의 경우, 하위 class에서 constructor가 없을 때, 메서드를 먼저 초기화한 뒤 상위 constructor를 찾아가기 때문에 하위 class의 메서드를 사용하게 됨.

super 키워드와 [[HomeObject]]

(생략)



9.3 정적 메서드와 정적 프로퍼티


요약

  • 정적 메서드는 특정 클래스 인스턴스가 아닌 클래스 '전체’에 필요한 기능을 만들 때 사용할 수 있습니다.
  • 인스턴스끼리 비교해주는 메서드 Article.compare(article1, article2)이나 팩토리 메서드 Article.createTodays()를 만들 때 정적 메서드가 쓰입니다.
  • 정적 메서드는 클래스 선언부 안에 위치하고 앞에 static이라는 키워드가 붙습니다.
  • 정적 프로퍼티는 데이터를 클래스 수준에 저장하고 싶을 때 사용합니다. 정적 프로퍼티 역시 개별 인스턴스에 묶이지 않습니다.
class MyClass {
  static property = ...;
  static method() {
    ...
  }
}
  • static을 사용한 선언은 기술적으론 클래스 자체에 직접 할당하는 것과 동일합니다.
MyClass.property = ...
MyClass.method = ...
  • 정적 프로퍼티와 정적 메서드는 상속이 가능합니다.
  • class B extends A는 클래스 B의 프로토타입이 클래스 A를 가리키게 합니다(B.[[Prototype]] = A). 따라서 B에서 원하는 프로퍼티나 메서드를 찾지 못하면 A로 검색이 이어집니다.
  • "prototype"이 아닌 클래스 함수 자체에 메서드를 설정할 수도 있습니다. 이런 메서드를 정적(static) 메서드라고 부릅니다.
class User {
  static staticMethod() {
    alert(this === User);
  }
}
User.staticMethod(); // true
  • 정적 메서드는 메서드를 프로퍼티 형태로 직접 할당하는 것과 동일한 일을 합니다.
class User { }
User.staticMethod = function() {
  alert(this === User);
};
User.staticMethod(); // true
  • User.staticMethod()가 호출될 때 this의 값은 클래스 생성자인 User 자체가 됩니다(점 앞 객체).
  • 정적 메서드는 어떤 특정한 객체가 아닌 클래스에 속한 함수를 구현하고자 할 때 주로 사용됩니다.
  • 객체 Article이 여러 개 있고 이들을 비교해줄 함수가 필요하다고 가정해 봅시다. 가장 먼저 아래와 같이 Article.compare를 추가하는 방법이 떠오를 겁니다.
class Article {
  constructor(title, date) {
    this.title = title;
    this.date = date;
  }

  static compare(articleA, articleB) {
    return articleA.date - articleB.date;
  }
}

// 사용법
let articles = [
  new Article("HTML", new Date(2019, 1, 1)),
  new Article("CSS", new Date(2019, 0, 1)),
  new Article("JavaScript", new Date(2019, 11, 1))
];

articles.sort(Article.compare);

alert( articles[0].title ); // CSS
  • 여기서 Article.comparearticle(글)을 비교해주는 수단으로, 글 전체를 ‘위에서’ 바라보며 비교를 수행합니다. Article.compare이 글 하나의 메서드가 아닌 클래스의 메서드여야 하는 이유가 여기에 있습니다.
class Article {
  constructor(title, date) {
    this.title = title;
    this.date = date;
  }

  static createTodays() {
    // this는 Article입니다.
    return new this("Today's digest", new Date());
  }
}

let article = Article.createTodays();

alert( article.title ); // Today's digest
  • 이제 Today’s digest라는 글이 필요할 때마다 Article.createTodays()를 호출하면 됩니다. 여기서도 마찬가지로 Article.createTodays()article의 메서드가 아닌 전체 클래스의 메서드입니다.

정적 프로퍼티

스펙에 추가된 지 얼마 안 된 문법입니다. 예시는 Chrome에서만 동작할 수 있습니다.

  • 정적 프로퍼티도 물론 만들 수 있습니다. 정적 프로퍼티는 일반 클래스 프로퍼티와 유사하게 생겼는데 앞에 static이 붙는다는 점만 다릅니다.
class Article {
  static publisher = "Ilya Kantor";
}

alert( Article.publisher ); // Ilya Kantor
  • 위 예시는 Article에 프로퍼티를 직접 할당한 것과 동일하게 동작합니다.
Article.publisher = "Ilya Kantor";

정적 프로퍼티와 메서드 상속

  • 정적 프로퍼티와 메서드는 상속됩니다.
class Animal {
  static planet = "지구";

  constructor(name, speed) {
    this.speed = speed;
    this.name = name;
  }

  run(speed = 0) {
    this.speed += speed;
    alert(`${this.name}가 속도 ${this.speed}로 달립니다.`);
  }

  static compare(animalA, animalB) {
    return animalA.speed - animalB.speed;
  }

}

// Animal을 상속받음
class Rabbit extends Animal {
  hide() {
    alert(`${this.name}가 숨었습니다!`);
  }
}

let rabbits = [
  new Rabbit("흰 토끼", 10),
  new Rabbit("검은 토끼", 5)
];

rabbits.sort(Rabbit.compare);

rabbits[0].run(); // 검은 토끼가 속도 5로 달립니다.

alert(Rabbit.planet); // 지구

  • extends 키워드는 Rabbit[[Prototype]]Animal을 참조하도록 해줍니다.
  • 따라서 Rabbit extends Animal은 두 개의 [[Prototype]] 참조를 만들어 냅니다.
  1. 함수 Rabbit은 프로토타입을 통해 함수 Animal을 상속받습니다.
  2. Rabbit.prototype은 프로토타입을 통해 Animal.prototype을 상속받습니다.
  • 이런 과정이 있기 때문에 일반 메서드 상속과 정적 메서드 상속이 가능합니다.
class Animal {}
class Rabbit extends Animal {}

// 정적 메서드
alert(Rabbit.__proto__ === Animal); // true

// 일반 메서드
alert(Rabbit.prototype.__proto__ === Animal.prototype); // true



📚 참고 : javascript.info

profile
protect me from what i want

0개의 댓글