JS - 클래스

정윤호·2024년 6월 27일
1

JS-Deep-Dive

목록 보기
7/7

클래스

이전에 프로토타입 체이닝을 통해 자바스크립트에서 상속이 구현됨을 확인했다.

var Person = (function(){
  function Person(name){
    this.name = name;
  }
  
  Person.prototype.sayHi = function(){
    console.log('안녕! 내 이름은 ' + this.name);
  }
return Person
}());

var me = new Person('Lee');
me.sayHi();

그런데, 많은 개발자들은 자바나 C#의 클래스 기반 객체지향 모델에 익숙하다. 이에 따라 ES6 에서는 클래스를 도입하여, 위 개발자들의 편의를 늘렸다.

그러나, 클래스는 껍데기만 클래스이지, 속살은 우리가 이미 아는 프로토타입 기반의 생성자 함수이다. 즉, 문법적 설탕에 해당한다. 다만, 기존 생성자 함수와는 완전히 일치하지는 않으므로 추가 공부가 필요하다.

클래스와 기존의 생성자 함수와의 차이점

  1. 클래스는 new 연산자 없으면 에러
  2. 클래스는 extends, super 키워드를 지원
  3. 클래스는 호이스팅이 안되는 것처럼 동작..! -> 사실은 일어난다는건가?
  4. 클래스 내부는 암묵적으로 strict mode 지정
  5. 클래스의 constructor, 메서드(프로토타입, 정적)는 모두 Enumerable 값이 false. 열거불가능.

클래스 정의

클래스는 일급 객체이다. 즉, 함수를 값처럼 쓰고 자유롭게 만지는 것 마냥, 클래스도 동일하게 다룰 수 있다. 클래스는 함수다.

class Person {
  constructor(name){
    this.name = name;
  }

  sayHi(){
    console.log('안녕 내 이름은 ' + this.name);
  }

  static sayHello(){
    console.log('아아안녕~');
  }
}

const me = new Person('윤호');
console.log(me.name); // '윤호'
me.sayHi(); // '안녕 내 이름은 윤호'
Person.sayHello(); // '아아안녕~'

클래스 호이스팅

함수처럼 런타임 이전에 평가되고 함수 객체를 생성한다. ([[JS - 빌트인 객체, this, 실행 컨텍스트]] 참조) -> constructor 함수와 프로토타입이 쌍으로 생성된다.

그런데, 클래스는 클래스 정의 이전에 호출이 불가능하다.

const Person = '';
{
  console.log(Person); // ReferenceError
  class Person {}
}

호이스팅이 안 일어났다면, Person 은 상위 스코프의 '' 값을 참조하는게 맞고, 출력도 멀쩡히 되야 한다.

그런데, 참조 에러가 발생한다. 이는 클래스도 호이스팅이 일어난다는 증거다. 그런데, let, const 키워드 처럼 일시적 사각지대에 빠지기 때문에 호이스팅이 발생하지 않는 것처럼 동작한다.

인스턴스 생성

클래스의 존재 의의는 인스턴스 생성이기에 항상 new 와 함께 사용된다.

const Person = class MyClass{};
console.log(MyClass); // 참조 오류
const you = new MyClass(); // 참조오류

위에서 에러가 발생하는 이유는 MyClass 라는 클래스 이름은 클래스 몸체 안에서만 유효하기 때문이다. 기명 함수 표현식과 비교하면 이해하기 쉽다.

const var = function foo(){
   return '사랑해요~';
}
console.log(var); // 함수
console.log(foo); // 참조오류

메서드

클래스가 가질 수 있는 메서드 종류는 세 가지. constructor, 프로토타입 메서드, 정적 메서드가 있다.

  • constructor

    클래스의 constructor 메서드와 프로토타입의 constructor 프로퍼티는 다르다!
    클래스 내부에서 두 개 이상의 consturctor 는 있을 수 없다.
    consturctor 는 생략 가능하다. 생략시 클래스는 빈 객체를 생성한다.
    constructor 에 return 은 없어야 한다. 기본적으로 return this; 해야 하니까. 이거 건들면 인스턴스 생성이 안됨. 다만 return 1; 같이 원시값을 반환하면, construuctor 는 이를 무시하고 this 를 정상적으로 반환한다. 하지만 역시 return 을 생략하는게 가장 안전하다..!

  • 프로토타입 메서드

    클래스는 prototype 프로퍼티에 메서드 추가하지 않아도 된다.
    생성자 함수와 마찬가지로 클래스로 생성한 인스턴스는 프로토타입 체이닝의 일원이 된다.
    클래스는 프로토타입 기반의 객체 생성 메커니즘일뿐, 생성자 함수와 역할이 동일하다.

  • 정적 메서드

    클래스는 정적 메서드를 생성자 함수와 다르게 명시적으로 표시한다. static 키워드를 붙인다.
    생성자 함수와 마찬가지로 정적 메서드는 인스턴스가 아닌 생성한 모체(생성자 함수/클래스)로 호출한다.
    this를 사용하지 않는 메서드라면, 정적 메서드를 쓰는게 좋다.

클래스의 인스턴스 생성 과정

  1. 빈 객체로 인스턴스 생성 + 여기에 this 바인딩
  2. 인스턴스 초기화 ~ constructor 내부 코드에 의해
  3. 인스턴스 반환 (인스턴스랑 바인딩된 this가 암묵적으로 반환)

프로퍼티

  1. 인스턴스 프로퍼티: constructor 안에서 정의
  2. 접근자 프로퍼티: getter와 setter. 프로토타입 프로퍼티. 인스턴스에서 참조 가능.
   class Person{
     get FullName(){
       returen `${this.firstName} ${this.lastName}`;
     }
     set FullName(name){
       [this.firstName, this.lastName] = name.split(' ');
     }
   }
  1. 클래스 필드
class Person{
  name = 'Lee';
  getName = function(){
    return this.name;
  }
}

클래스 필드에 함수 선언하면, 이는 프로토타입 메서드가 아니라 인스턴스 메서드가 된다. 즉 인스턴스 만들때마다 새로 함수를 만들어 내니까, 하지 마라탕...
4. private field

   class Person = {
     #name = ''; // 얘는 클래스 몸체 안에서만 참조 가능
     constructor(name){
       this.#name = name;
     }
     get name(){
       return this.#name;
     }
   }

인스턴스에서 접근 못하는 프로퍼티 만들 때 사용. 접근자 프로퍼티로 간접적으로 접근은 가능.
프라이빗 필드는 반드시 클래스 몸체에서만 정의해야 함...

클래스 상속

class Animal {
  constructor(age, weight){
    this.age = age;
    this.weight = weight;
  }

  eat(){return 'eat';}

  move(){return 'move';}
}

class Bird extends Animal {
  fly(){return 'fly';}
}

class Dog extends Animal {
  bark(){return '멍';}
}

상속을 주는 Animal 은 수퍼 클래스 / 상속을 받는 Bird 나 Dog 은 서브클래스
프로토타입 메서드는 물론, 정적 메서드도 상속 가능!

동적 상속

class 가 생성자 함수에게 상속을 받을 수 있다.
class Derived extends Base:생성자 함수 {}

서브클래스의 constructor

constructor 생략하면 빈 constructor 가 암묵적으로 정의되고, 얘는 빈 객체를 반환한다.
그런데... 위에서 처럼 서브클래스에서 생략하면

class Tiger extends Animal {
  constructor(...args){ super(...args) }
}

사실 위의 코드가 암묵적으로 위 같은 constructor가 정의된다.
super() 는 수퍼클래스의 constructor 를 호출하여 인스턴스를 생성한다.

super

super 키워드는 함수처럼 호출도 되고, this 처럼 식별자처럼 참조도 된다.
1. 호출하면 수퍼클래스의 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;
  }
}

서브 클래스에서 constructor 를 정의하면, 위위에 있는 암묵적인 super 호출 정의가 오버라이드 되므로, super를 호출해야 한다.
super 호출은 웬만하면 constructor의 최상단
그리고 얘는 반드시 서브클래스에서만 호출

  1. 참조하면 수퍼클래스의 메서드를 호출
class Base{
  constructor(name){
    this.name = name;
  }

  sayHi(){
    return `안녕! ${this.name}`;
  }
}

class Derived extends Base {
  sayHi(){
    return `${super.sayHi()}, 어떻게 지내!` // 메서드 오버라이딩
  }
}

여기서 super.sayHi 는 사실상 Base.prototype.sayHi 를 의미한다. 단, this 바인딩 면에서 다른 점이 있으니 조심해야 한다.

상속 클래스의 인스턴스 생성 과정 (몰라서 넘어감. 다음에 딥다이브해야지)

  1. 서브 클래스의 super 호출. 수퍼클래스한테 인스턴스 생성 위임. 해줘잉.
  2. 수퍼 클래스의 인스턴스 생성과 this 바인딩
  3. 수퍼클래스의 인스턴스 초기화
  4. 서브 클래스 constructor 로의 복귀와 this 바인딩
  5. 서브클래스의 인스턴스 초기화
  6. 인스턴스 반환

표준 빌트인 생성자 함수 확장

extends 는 생성자 함수에게도 상속을 받을 수 있다.
class MyArray extends Array{}
하면, 내가 원하는 추가 메서드를 더한 배열 인스턴스를 생성할 수 있다.

profile
우리 인생 화이팅~

0개의 댓글