[JS] 클래스

Pavel_Dmr·2022년 6월 27일
0

JavaScript

목록 보기
9/9

🌴 클래스는

자바스크립트에서 클래스는 이전에 있었던 생성자 함수을 대체하기 위해서 ES6에 나온 개념입니다.

그전에는 생성자 함수을 이용해 객체를 생성하곤 했습니다.

생성자 함수나 프로토타입 개념으로도 객체을 생성하고 확장 할 수 있지만, 클래스을 이용하면 좀 더 다양하게 이용 할 수 있습니다.

프로토타입과 비슷한 특징을 가지고있고, 클래스을 이용하면 생성자 함수을 이용하는 거보다 더 깔끔한 문법으로 정의 할 수 있습니다. 또한 상속 개념으로 좀더 쉽게 개념을 확장 할 수 있습니다.

자바스크립트에서 클래스는 기존 클래스 기반 객체지향 프로그래밍 언어와 매우 흡사하게 객체 생성 메커니즘을 구현합니다.

또한 기존의 함수와 객체와 다른 개념이니 혼동하면 안됩니다.

// 클래스는 함수가 아닙니다!
class Person {
  console.log('hello');
}
// 에러: Unexpected token

// 클래스는 객체가 아닙니다!
class Person {
  prop1: 1,
  prop2: 2
}
// 에러: Unexpected token

🌻 클래스 정의하기

클래스에서 생성자는 constructor라는 이름으로 정의합니다. 또한 메소드 정의시 메소드 명과 파라미터만 적습니다.function 사용시 토큰 오류가 발생합니다.

// 클래스
class Person {
  // 이전에서 사용하던 생성자 함수는 클래스 안에 `constructor`라는 이름으로 정의합니다.
  constructor({name, age}) {
    this.name = name;
    this.age = age;
  }

  // 객체에서 메소드를 정의할 때 사용하던 문법을 그대로 사용하면, 메소드가 자동으로 `Person.prototype`에 저장됩니다.또한 토큰 오류 발생함
  introduce() {
    return `안녕하세요, 제 이름은 ${this.name}입니다.`;
  }
}

const person = new Person({name: '김민수', age: 19});

또한 호이스팅이 발생하지 않는것처럼 동작하기 때문에(정확하겐 일시적 사각지대 TDZ에 빠져서 발생하지 않는 것처럼 보인다. let const와 동일하게 선언과 초기화 단계사이에 빠지기때문에), 클래스 선언문은 참조보다 앞에 와야한다.

console.log(Foo);
// ReferenceError: Cannot access 'Foo' before initialization

class Foo {}
const Foo = '';

{
  // 호이스팅이 발생하지 않는다면 ''가 출력되어야 한다.
  console.log(Foo);
  // ReferenceError: Cannot access 'Foo' before initialization
  class Foo {}
}

호이스팅이 되지만 TDZ에 빠지기 때문에 다음과 같이 레퍼런스 오류가 뜬다.


🍦 클래스 인스턴스 생성

클래스는 new 연산자 없이 호출이 불가능합니다. 고로 인스턴스 생성시 반드시 new로 호출 해야합니다.

class Foo {}

const foo = new Foo();

또한 이렇게 생성한 foo는 클래스 자체가 아니라 constructor입니다. 표현식이 아닌 선언식으로 정의한 클래스의 이름은 constructor와 동일합니다.

console.log(Object.getPrototypeOf(foo).constructor === Foo); // true (생성자로 확인이 된다)

위에서 설명했다싶이, new 연산자 없이 constructor를 호출하면 타입 에러가 발생합니다.

class Foo {}

const foo = Foo(); // TypeError: Class constructor Foo cannot be invoked without 'new'

클래스 생성자

앞에서 정의하기에서 constructor는 인스턴스를 생성하고 클래스 필드(클래스의 변수)를 초기화하기위해 클래스에 존재하는 특수한 메소드의 개념입니다.

**클래스 필드
클래스 내부의 캡슐화된 변수를 말합니다. 데이터 멤버,멤버 변수라고도 부릅니다. 클래스 필드는 인스턴스의 프로퍼티또는 정적 프로퍼티가 될 수 있습니다.

더 쉽게 설명하면 우리가 자바스크립트에서 기존에 객체을 생성할 때, 생성자 함수에서 this에 추가한 프로퍼티를 클래스 기반 언어에서는 클래스 필드라고 부릅니다.

// 클래스 선언문
class Person {
  // constructor(생성자). 이름을 바꿀 수 없다.
  constructor(name) {
    // this는 클래스가 생성할 인스턴스를 가리킨다.
    // _name은 클래스 필드이다.
    this._name = name;
  }
}

// 인스턴스 생성
const me = new Person('Lee');
console.log(me); // Person {_name: "Lee"}

constructor는 클래스 내에 반드시 "1개"만 존재해야하고,2개이상 존재시 문법 오류가 발생합니다. new로 인스턴스 생성시 constructor의 파라미터에 전달한 값은 클래스 필드에 할당됩니다.

또한 constructor는 생략할 수 있습니다. 생략하면 constructor() {}을 포함한 것과 동일하게 동작합니다. 즉 생략하고, 인스턴스 생성시 빈 객체가 생성됩니다. 인스턴스에 프로퍼티를 추가하는건, 기존 객체에 프로퍼티을 동적으로 추가하는 방식과 같습니다.

class Foo { }

const foo = new Foo();
console.log(foo); // Foo {}

// 프로퍼티 동적 할당 및 초기화
foo.num = 1;
console.log(foo); // Foo { num: 1 }

다만 이러한 방식은, 클래스 사용하는 의미가 없다시피 하기때문에, constructor을 꼭 잊지말고 정의하자. 특정 프로퍼티가 필요할때, 기존 객체처럼 동적으로 추가할 수 있다는 것만 알면된다.


🍸 클래스 필드

클래스 바디에는 메소드만 선언할 수 있습니다. 고로 클래스 필드는 반드시 contructor 내부에 작성해야 합니다. 또한 접근자을 지정하지 않으면 기본적으로 public으로 지정되어, 클래스 인스턴스을 통해 외부에서 참조가 가능합니다. 접근 제한자,메모리 할당 개념은 원래 모듈을 통해 구현 해야 했지만, 표준안이 제안되면서 몇몇 최신 브라우저나 Node.js에서 사용가능합니다.메모리할당방식 static은 변수 명앞에, 접근자 private는 변수에 #을 붙여서 지정합니다.

class Foo {
  x = 1;            // Field declaration
  #p = 0;           // Private field
  static y = 20;    // Static public field
  static #sp = 30;  // Static private field
  // static #sm() {    // Static private method
  //   console.log('static private method');
  // }

  bar() {
    this.#p = 10; // private 필드 참조
    // this.p = 10; // 새로운 public p 필드를 동적 추가한다.
    return this.#p;
  }
}

const foo = new Foo();
console.log(foo); // Foo {#p: 10, x: 1}

console.log(foo.x); // 1
// console.log(foo.#p); // SyntaxError: Undefined private field #p: must be declared in an enclosing class
console.log(Foo.y); // 20
// console.log(Foo.#sp); // SyntaxError: Undefined private field #sp: must be declared in an enclosing class
console.log(foo.bar()); // 10

🥙 클래스 getter,setter

getter는 클래스 필드에 접근할 떄마다, 필드의 값을 조작하는 수행이 필요할 때 사용합니다. getter는 메소드 이름 앞에 get 키워드를 사용해 정의합니다. 이때 메소드 이름은 클래스 필드 이름처럼 사용됩니다. getter는 호출하는 것이 아니라 프로퍼티처럼 참조하는 형식입니다. 참조 시에 메소드가 호출됩니다. getter는 명칭 그대로 무언가를 취득 할 때 사용하므로 반드시 결과물을 반환해야 합니다.

class Foo {
  constructor(arr = []) {
    this._arr = arr;
  }

  // getter: get 키워드 뒤에 오는 메소드 이름 firstElem은 클래스 필드 이름처럼 사용된다.
  get firstElem() {
    // getter는 반드시 무언가를 반환해야 한다.
    return this._arr.length ? this._arr[0] : null;
  }
}

const foo = new Foo([1, 2]);
// 필드 firstElem에 접근하면 getter가 호출된다.
console.log(foo.firstElem); // 1

setter는 클래스 필드에 값을 할당할 때마다 클래스 필드의 값을 조작하는 행위가 필요 할 때 마다 사용합니다. setter는 메소드 이름 앞에 set 키워드를 사용해 정의합니다. 참조 방식은 getter와 동일합니다.


  // getter: get 키워드 뒤에 오는 메소드 이름 firstElem은 클래스 필드 이름처럼 사용된다.
  get firstElem() {
    // getter는 반드시 무언가를 반환하여야 한다.
    return this._arr.length ? this._arr[0] : null;
  }

  // setter: set 키워드 뒤에 오는 메소드 이름 firstElem은 클래스 필드 이름처럼 사용된다.
  set firstElem(elem) {
    // ...this._arr은 this._arr를 개별 요소로 분리한다
    this._arr = [elem, ...this._arr];
  }
}

const foo = new Foo([1, 2]);

// 클래스 필드 lastElem에 값을 할당하면 setter가 호출된다.
foo.firstElem = 100;

console.log(foo.firstElem); // 100

getter,setter는 클래스 필드에 접근하기 위한 접근자 프로퍼티라고 생각하면 됩니다. 실제로도 프로퍼티입니다. 선언한 클래스의 프로토타입 프로퍼티로 확인됩니다.

// firstElem은 접근자 프로퍼티이다. 접근자 프로퍼티는 프로토타입의 프로퍼티이다.
console.log(Object.getOwnPropertyDescriptor(Foo.prototype, 'firstElem'));
// {get: ƒ, set: ƒ, enumerable: false, configurable: true}

🥧 클래스의 정적 메소드

위에서 클래스 필드에서 메모리 할당 방식을 정적으로 바꿔주는 static을 알려주었는데, 정적 메소드는 클래스의 인스턴스가 아닌 클래스 이름으로 호출한다. 고로 클래스의 인스턴스를 생성하지 않아도 호출가능하고, this을 사용할 수 없다.

달리말하면 this을 사용할 필요가 없는 메소드는 정적 메소드로 만들 수 있다. 또한 정적 메소드는 Math 객체의 메소드처럼 애플리케이션 전역에서 사용할 유틸리티 함수를 생성할 때 주로 사용한다.

class Foo {
  constructor(prop) {
    this.prop = prop;
  }

  static staticMethod() {
    /*
    정적 메소드는 this를 사용할 수 없다.
    정적 메소드 내부에서 this는 클래스의 인스턴스가 아닌 클래스 자신을 가리킨다.
    */
    return 'staticMethod';
  }

  prototypeMethod() {
    return this.prop;
  }
}

// 정적 메소드는 클래스 이름으로 호출한다.
console.log(Foo.staticMethod());

const foo = new Foo(123);
// 정적 메소드는 인스턴스로 호출할 수 없다.
console.log(foo.staticMethod()); // Uncaught TypeError: foo.staticMethod is not a function

static을 사용하면 해당 클래스 고유의 것이 되기 때문에, 인스턴스을 통해 호출 할 수 없다. 또한 상속 개념에서 자식클래스 또한 static 클래스을 상속 받을 수 없다. 만약 인스턴스나 자식 클래스에서 static 메소드을 사용하고 싶다면, 해당 클래스의 메소드 자체을 직접 호출 해야한다.


🥫 클래스의 상속

클래스 상속(Class Inheritance)는 코드 재사용 관점에서 매우 유용합니다. 새롭게 정의할 클래스가 기존에 있는 클래스와 매우 유사하다면, 상속을 통해 그대로 사용하되 다른 점만 구현하면 된다. 코드 재사용은 개발 비용을 현저히 줄일 수 있는 잠재력이 있으므로 매우 중요하다.

// 부모 클래스
class Circle {
  constructor(radius) {
    this.radius = radius; // 반지름
  }

  // 원의 지름
  getDiameter() {
    return 2 * this.radius;
  }

  // 원의 둘레
  getPerimeter() {
    return 2 * Math.PI * this.radius;
  }

  // 원의 넓이
  getArea() {
    return Math.PI * Math.pow(this.radius, 2);
  }
}

// 자식 클래스
class Cylinder extends Circle {
  constructor(radius, height) {
    super(radius);
    this.height = height;
  }

  // 원통의 넓이: 부모 클래스의 getArea 메소드를 오버라이딩하였다.
  getArea() {
    // (원통의 높이 * 원의 둘레) + (2 * 원의 넓이)
    return (this.height * super.getPerimeter()) + (2 * super.getArea());
  }

  // 원통의 부피
  getVolume() {
    return super.getArea() * this.height;
  }
}

// 반지름이 2, 높이가 10인 원통
const cylinder = new Cylinder(2, 10);

// 원의 지름
console.log(cylinder.getDiameter());  // 4
// 원의 둘레
console.log(cylinder.getPerimeter()); // 12.566370614359172
// 원통의 넓이
console.log(cylinder.getArea());      // 150.79644737231007
// 원통의 부피
console.log(cylinder.getVolume());    // 125.66370614359172

// cylinder는 Cylinder 클래스의 인스턴스이다.
console.log(cylinder instanceof Cylinder); // true
// cylinder는 Circle 클래스의 인스턴스이다.
console.log(cylinder instanceof Circle);   // true

위 코드에서 Circle이라는 부모클래스가 있고, extends라는 키워드을 통해, 상속 받은 자식 클래스 Cylinder가 있다. 자식 클래스에서는 static 클래스필드,메소드을 제외하고는 전부 상속 받아 사용할 수 있습니다. 또한 다양한 필요에 따라 메소드을 재정의해 사용 할 수 있습니다. 이를 메소드 오버라이딩이라고 합니다.

오버라이딩(Overriding)
상위 클래스가 가지고 있는 메소드를 하위 클래스가 재정의하여 사용하는 방식이다.

오버로딩(Overloading)
매개변수의 타입 또는 갯수가 다른, 같은 이름의 메소드를 구현하고 매개변수에 의해 메소드를 구별하여 호출하는 방식이다. 자바스크립트는 오버로딩을 지원하지 않지만 arguments 객체를 사용하여 구현할 수는 있다.

아무튼 이러한 상속관계을 기존의 프로토타입 체인의 변수,메소드 접근과 비교하면 다음과 같다.

프로토타입 체인은 특정 객체의 프로퍼티나 메소드에 접근하려고 할 때 프로퍼티 또는 메소드가 없다면 [[Prototype]] 내부 슬롯이 가리키는 링크를 따라 자신의 부모 역할을 하는 프로토타입 객체의 프로퍼티나 메소드를 차례대로 검색한다. 그리고 검색에 성공하면 그 프로퍼티나 메소드를 사용한다.

또한 상속관계에서는 super라는 키워드가 있습니다.

super 키워드는 부모 클래스를 참조(Reference)할 때 또는 부모 클래스의 constructor를 호출할 때 사용한다.

위 “extends 키워드”의 예제를 보면 super가 메소드로 사용될 때, 그리고 객체로 사용될 때 다르게 동작합니다.

// 부모 클래스
class Circle {
...
}

class Cylinder extends Circle {
  constructor(radius, height) {
    // ① super 메소드는 부모 클래스의 constructor를 호출하면서 인수를 전달한다.
    super(radius);
    this.height = height;
  }

  // 원통의 넓이: 부모 클래스의 getArea 메소드를 오버라이딩하였다.
  getArea() {
    // (원통의 높이 * 원의 둘레) + (2 * 원의 넓이)
    // ② super 키워드는 부모 클래스(Base Class)에 대한 참조
    return (this.height * super.getPerimeter()) + (2 * super.getArea());
  }

  // 원통의 부피
  getVolume() {
    // ② super 키워드는 부모 클래스(Base Class)에 대한 참조
    return super.getArea() * this.height;
  }
}

// 반지름이 2, 높이가 10인 원통
const cylinder = new Cylinder(2, 10);

자식 클래스의 constructor에서 super()을 통해서 부모 클래스의 생성자을 반드시 먼저 호출 해야 한다.(자바에서는 없으면 자동으로 생성해준다. 자바스크립트는 꼭 직접 써줘야한다.) 그렇게 하지 않으면 "this" 참조 오류가 난다.

또한 메소드 오버라이딩한 직후, 원래 부모클래스의 메소드가 필요하면 super.method명() 통해 호출한다.

static, prototype 메소드 상속

class Parent {}

class Child extends Parent {}

console.log(Child.__proto__ === Parent); // true
console.log(Child.prototype.__proto__ === Parent.prototype); // true

자식 클래스의 프로토타입은 부모 클래스이다. static 메소드는 인스턴스에서 호출하지 못하지만, 프로토타입에 의해서 상속은 된다. 자식 클래스에서도 메소드 내부 또는 인스턴스에서 호출하는 것을 제외하고, 호출은 가능하다.

class Parent {
  static staticMethod() {
    return 'staticMethod';
  }
}

class Child extends Parent {}

console.log(Parent.staticMethod()); // 'staticMethod'
console.log(Child.staticMethod());  // 'staticMethod

메소드 오버라이딩도 가능하다. 프로토타입 체인에 의해서 여전히 인스턴스에서는 호출 할수가 없다.

class Parent {
  static staticMethod() {
    return 'Hello';
  }
}

class Child extends Parent {
  static staticMethod() {
    return `${super.staticMethod()} wolrd`;
  }

  prototypeMethod() {
    return `${super.staticMethod()} wolrd`;
  }
}

console.log(Parent.staticMethod()); // 'Hello'
console.log(Child.staticMethod());  // 'Hello wolrd'
console.log(new Child().prototypeMethod());
// TypeError: (intermediate value).staticMethod is not a function
profile
노는게 좋아

0개의 댓글