[클래스]

1. 클래스 기본문법

  class MyClass {
    // 여러 메서드를 정의할 수 있음
    constructor() { ... }
    method1() { ... }
    method2() { ... }
    method3() { ... }
    ...
  }

*constructor는 new 에 의해 자동으로 호출됨, 특별한 절차 없이 객체 초기화 가능 예를들어

  class User {

    constructor(name) {
      this.name = name;
    }
  let user = new User("John");

이렇게 생성했다면 constructor()가 실행되어 this.name 에 John이 들어가게 된다.

  • 클래스란 일단 내가아는 개념. 객체가 붕어빵이라면 클래스는 붕어빵틀이다. 개체를 만들기 위한 틀을 만드는 것이라 생각한다.

class User {} 문법구조의 하는 일

  1. User라는 이름을 가진 함수를 만듬. 함수 본문은 constructor에서 가져옴. (생성자 메서드가 없으면 본문이 비워진 채로 함수가 만들어짐)
  2. sayHi같은 클래스 내에서 정의한 메서드를 User.prototype에 저장함. (결국, 클래스안의 constructor 부터 시작해서 메서드들은 클래스.prototype에서 가져오는 것이다.)

※ 편의 문법 기능은 동일하지만 조금더 읽기 쉽게 하기위해 쓰는 문법 (문법 설탕이라고도 한다 함.)

class를 명시하지 않아도 클래스를 생성할 수 있는 듯하여 클래스를 편의 문법이라고 착각하는 경우가 있으나 class는 편의 문법이 아니다.

function으로 객체를 만드는 방법으로 사용해도 될듯하지만 다른 점이 있다. 1) class 로 만든 함수에는 [[FunctionKind]]:”classConstructor”가 이름표처럼 붙음 => new를 사용하지 않으면 클래스 사용 불가 2) 클래스 메서드는 열거할수 없다. (non-enumerable) for in 순회 할 때 제외하는 경우가 많음. 3) 항상 엄격모드로 실행된다.

  • 클래스 표현식 함수 표현식과 비슷하게 내부에서 정의,반환,할당 가능하다.
  function makeClass(phrase) {
    // 클래스를 선언하고 이를 반환함
    return class {
      sayHi() {
        alert(phrase);
      };
    };
  }
  
  // 새로운 클래스를 만듦
  let User = makeClass("Hello");
  
  new User().sayHi(); // Hello

위의 같이 함수 안에서 반환되는 결과로 클래스를 반환할 수 있고 User에 makeClass를 반환하여 넣으면 class를 만들어 그안에 메서드 sayHi()를 만들어 반환해준다. 그러면 User().sayHi() 를 사용할 수 있다.

  • 계산된 메서드 이름 ```javascript class User {

    ‘say’ + ‘Hi’ { alert(“Hello”); }

}

new User().sayHi();

  위의 경우 sayHi라는 메서드가 생기는 것이다.

  - 클래스 필드
```javascript
  class User{
    name = 'John';
  }

위와 같이 그냥 프로퍼티처럼 할당해주면 된다.

  • 클래스 필드의 특징은 클래스 자체에는 값이 저장되는 것이 아니라 실제 객체가 생겼을 때 값을 저장시킨다는 것이다.
  • 클래스 필드에는 함수의 결과 값이 오는 것도 가능하다.

  • 클래스 바인딩 클래스 바인딩을 보기 전에 먼저 함수의 바인딩에서 ‘사라진 this’에 대한 이슈를 다시 점검할 필요가 있다.
    //다시보는_함수_바인딩
    let user = {
      firstName: "John",
      sayHi() {
        alert(`Hello, ${this.firstName}!`);
      }
    };
    
    setTimeout(user.sayHi, 1000); // Hello, undefined!

위의 코드에서 user.sayHi 안에 this를 호출할 때 this 값을 제대로 못가져와 undefined로 출력된다. 위 setTimeout 코드는 다음과 같다.

    let f = user.sayHi;
    setTimeout(f, 1000); // user 컨텍스트를 잃어버림

user.sayHi 라는 그 함수만 따로 떼어와서 넣기에 거기에 해당하는 this.firstName이 뭐가되는가… setTimeout 의 특성상 this에는 window를 할당한다… this.firstName 은 window.firstName 이 되므로 undefined를 출력하는 것이다.

이를 해결하는 방법! 1) 래퍼

    setTimeout(function() {
      user.sayHi(); // Hello, John!
    }, 1000);

위와 같이 앞에 function() 코드를 써주는 것이다.

그러면 외부렉시컬 환경에서 user를 찾아주게 된단다. 그러면 user 안에서의 this도 쓸 수 있는 것이다.

2) bind 사용하기 모든 함수는 this를 수정하게 해주는 bind 메서드를 제공해준다. 위의 경우에

    let user = {
      firstName: "John",
    };

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

    let SayHi = sayHi.bind(user)
    setTimeout(user.sayHi, 1000); // Hello, undefined!

이렇게 해주면 sayHi를 user로 바인딩하여 해당하는 this를 user로 고정시키겠다는 것이다.

    //다시_클래스_바인딩으로_돌아와서
    클래스는 바인딩을  훌륭한 방법으로 제공한다.

    class Button {
      constructor(value) {
        this.value = value;
      }
      click = () => {
        alert(this.value);
      }
    }
    
    let button = new Button("hello");
    
    setTimeout(button.click, 1000); // hello

위의 코드에서 클래스의 함수는 객체 마다 각각 만들어진다. 그러므로 함수안에 this는 각 객체를 가르킨다.

위의 경우에 button 객체에 해당 메서드 뿐 아니라 값이 같이 들어가게 된다.

  • button의 값
    value : "hello"
    click(){
      alert(this.value)
    }
    

    위와 같으므로 실행되는데 이상이 없다.

2. 클래스 상속

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

클래스 간에 상속하게 되면 기존 기능에 추가적인 기능으로 사용할 수 있다. 사용법은 extends obj (상속해주는 클래스명)

extends는 Rabbit.prototype에 Animal 을 넣게 됨. 어떤 메서드를 호출할 때 검색 순서는 당연히 가까운 프로토타입 프로퍼티 부터 먼데 순.

  • 메서드 오버라이딩 상속받는 객체에 부모 메서드와 이름이 같은 메서드가있으면 오버라이딩이고 그 메서드를 우선 호출한다. 이 때 부모 메서드를 호출하고 싶다면 super를 사용한다. super.method()

  • 생성자 오버라이딩
      class Rabbit extends Animal {
    
    constructor(name, earLength) {
      super(name);
      this.earLength = earLength;
    }
      
    // ...
      }
    

    위와 같이 Rabbit에서 생성자 constructor를 만들고 싶은 경우에는 반드시 먼저 super() 를 사용해서 부모 생성자를 호출해 주어야 한다. 그 이유는, 상속받은 클래스의 생성자로 객체를 만들 때, 상속에서 파생된 애들은 구분해주기 때문이다. [[ConstructorKind]]:”derived”

  • 초기화 순서에 관하여 1) 아무것도 상속받지 않는 베이스 클래스는 생성자 실행 이전에 초기화됨 2) 부모 클래스가 있는 경우엔 super() 실행 직후에 초기화됨 (중요!)

상속받는 클래스의 경우 클래스 필드에서 초기화 한 변수가 똑같이 부모 클래스에서도 있을 경우 부모 클래스 변수 값을 사용한다.

  • super는 HomeObject 라는 특수 내부 프로퍼티에서 클래스의 정체성을 가져와서 사용하기 때문에 유연한 사용이 가능하다. ```javascript let animal = { name: “동물”, eat() { // animal.eat.[[HomeObject]] == animal alert(${this.name} 이/가 먹이를 먹습니다.); } };

let rabbit = { proto: animal, name: “토끼”, eat() { // rabbit.eat.[[HomeObject]] == rabbit super.eat(); } };

let longEar = { proto: rabbit, name: “귀가 긴 토끼”, eat() { // longEar.eat.[[HomeObject]] == longEar super.eat(); } };

// 이제 제대로 동작합니다 longEar.eat(); // 귀가 긴 토끼 이/가 먹이를 먹습니다.

  위의 코드를 하나씩 분석해보면...
  1) longEar의 super 면 rabbit이 된다. 그러면 rabbit.eat() 을 가져온다.
  2) rabbit.ear() 가져오니 super.eat() 이 있다. 근데 여기서 super는 longEar 영역에서 사용되었으므로 rabbit이 받아야 할 것 같은데...
  3) 아니다. super는 [[HomeObject]] 에서 저장한 현재 객체 정보를 사용하여 rabbit의 super로 인식하여 animal로 반환해준다.
  4) 그러므로 결과적으로 animal.eat()이 실행되어 alert ... 문이 실행된다.
  5) 여기서 this.name 값은 longEar의 name인 "귀가 큰 토끼" 가 된다.

  그렇기 때문에 super는 동적인 사용이 안되고 해당 클래스의 고정된 부모 메서드를 가져오는 것이므로 주의해야한다.
  
  * 또한 super.method() 를 사용할 때는 부모 클래스에서 method() { } 식으로 정의해둬야지 method : function() 식으로 정의하면 이를 프로퍼티로 인식하여 오류난다.


### 3. 정적 메서드와 정적 프로퍼티
  - 정적 메서드 : prototype에 설정하는 것이 아니라 클래스 함수 자체에 메서드를 설정해주는 메서드
  어떤 특정한 객체가 아닌 클래스에 속한 함수를 구현하고자 할 때 주로 사용됨

```javascript
  class User { }

  User.staticMethod = function() {
    alert(this === User);
  };

  User.staticMethod(); // true

  this는 User 자체가 된다.
  • 정적 프로퍼티 : 그냥 프로퍼티랑 별 다를게 없음 앞에 static 붙는 것 외에는.

정적 메서드, 프로퍼티도 extends 로 상속이 적용되며 그를 위해서 자식 클래스에선 [[prototype]] 이 두개가 만들어진다고 보면 된다. 정적인 애들을 상속받는 정적[[prototype]] 그 외에 클래스.prototype 안에 있는 애들을 상속받는 기존의 [[prototype]]

4. private, protected 프로퍼티와 메서드

객체 지향 프로그래밍의 중요 원리 : “내부 인터페이스와 외부 인터페이스를 구분하겠다는 것.”

  • 내부 인터페이스와 외부 인터페이스
  • 내부 인터페이스(internal interface) – 동일한 클래스 내의 다른 메서드에선 접근할 수 있지만, 클래스 밖에선 접근할 수 없는 프로퍼티와 메서드
  • 외부 인터페이스(external interface) – 클래스 밖에서도 접근 가능한 프로퍼티와 메서드

  • public: 어디서든지 접근할 수 있으며 외부 인터페이스를 구성. 기본적으로 프로퍼티는 public 임
  • private: 클래스 내부에서만 접근할 수 있으며 내부 인터페이스를 구성할 때 쓰임.

** protected 프로퍼티 명 앞엔 밑줄 _을 붙여서 사용

  • 읽기 전용 프로퍼티 만들기
      class CoffeeMachine {
    // ...
    constructor(power) {
      this._power = power;
    }
    get power() {
      return this._power;
    }
      }
    

    power 는 getter 만 썼으므로 불러오는 것은 가능하지만 변경되는 것은 불가능 (에러 발생)

  • private 프로퍼티 private 프로퍼티와 메서드는 #으로 시작
  // 클래스 외부에서 private에 접근할 수 없음
  coffeeMachine.#checkWater(); // Error
  coffeeMachine.#waterLimit = 1000; // Error

상속을 통해서도 접근 못함 오직 접근 방법은 해당 클래스 내에서 getter, setter를 사용하여 접근해야 함.

그래서 protected 를 더 자주 쓰게 됨

5. 내장 클래스 확장하기

내장 클래스 인 배열, map 도 extends로 상속 받을 수 있다. 내장 클래스를 상속한 경우는 내장 클래스의 정적 메서드를 상속해주지는 않는다. 그러면 막장 됨. 상속의 최고봉은 누구냐 Object 라는 애다. 얘의 대표적인 정적 메서드가 key라는애야 근데 그럼 Array는 Object를 상속받으니까 Array.key를 사용할 수 있다. 그건 안된다는거임

6. Instanceof 로 클래스 확인하기

instanceof 연산자를 사용하면 객체가 특정 클래스에 속하는지 아닌지를 확인할 수 있음 (상속 관계도 확인 가능)

  • 사용법
      obj instanceof Class
    

    obj가 Class에 속하거나 Class를 상속받는 클래스에 속하면 true가 반환됨

      alert( rabbit instanceof Rabbit ); // true
    

    클래스 뿐 아니라 생성자 함수에서도 사용 가능

      alert( new Rabbit() instanceof Rabbit ); // true
    

    내장클래스에서도 사용 가능 (Array, Object)

7. 믹스인

원래는 클래스 하나 당 프로토타입 하나만 가질 수 있는데 예를들어 청소차랑 오토바이를 합쳐서 청소 오토바이를 만들고 싶다 하면?? 이 때 필요한게 믹스인!

  Object.assign(User.prototype, sayHiMixin);

위를 사용하면 User에서 sayHiMixin 의 메서드 들을 쓸 수 있다. 그러면 다른 객체는 상속 받고 다른 객체에선 메서드 복사하고 그런식으로 사용 가능하다.

또한 User 에다가 sayHiMixin 안의 메서드를 복사한 건데 sayHiMixin 에서 사용된 메서드가 다른 객체를 상속받게되어 super.method()를 사용한다해도 정상동작한다. (super는 동적으로 동작하지 않기 때문임)