[JavaScript] 4-2. JavaScript Class

JavaScript Class


ES2015 Class

ES2015 이전까지는 비슷한 종류의 객체를 많이 만들어내기 위해 생성자를 사용해왔습니다.

// 생성자
function Person({ name, age }) {
  this.name = name;
  this.age = age;
}
Person.prototype.introduce = function() {
  return `안녕하세요, 제 이름은 ${this.name}입니다.`;
};

const person = new Person({ name: "윤아준", age: 19 });
console.log(person.introduce()); // 안녕하세요, 제 이름은 윤아준입니다.
console.log(typeof Person); // function
console.log(typeof Person.prototype.constructor); // function
console.log(typeof Person.prototype.introduce); // function
console.log(person instanceof Person); // true

ES2015 에서 도입된 클래스는 생성자의 기능을 대체합니다.
class 표현식을 사용하면, 생성자와 같은 기능을 하는 함수를 훨씬 더 깔끔한 문법으로 정의할 수 있습니다.

// 클래스
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 });
console.log(person.introduce()); // 안녕하세요, 제 이름은 윤아준입니다.
console.log(typeof Person); // function
console.log(typeof Person.prototype.constructor); // function
console.log(typeof Person.prototype.introduce); // function
console.log(person instanceof Person); // true

class 블록에서는 JavaScript 의 다른 곳에서는 사용되지 않는 별도의 문법으로 코드를 작성해야 합니다.
함수 혹은 객체의 내부에서 사용하는 문법과 혼동하지 않도록 주의가 필요합니다.

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

문법이 아니라 동작방식의 측면에서 보면, ES2015 이전의 생성자와 ES2015 의 클래스는 다음과 같은 차이점이 있습니다.



메소드 정의하기

클래스의 메소드를 정의할 때는 객체 리터럴에서 사용하던 문법과 유사한 문법을 사용합니다.
인스턴스 메소드(instance method)는 다음과 같은 문법을 통해 정의합니다.

// Calculator라는 메소드를 정의하였으므로 add와 subtract라는 인스턴스 메소드를 사용할 수 있습니다.

class Calculator {
  add(x, y) {
    return x + y;
  }
  subtract(x, y) {
    return x - y;
  }
}

// 사용방법은 기존 생성자와 비슷합니다
const c = new Calculator();
c.add(2, 3); //  2+3 = 5
c.subtract(5, 3); //  5-3 = 2

// 클래스 안에서 인스턴스 메소드를 만들면 해당 클래스안에 프로토타입 함수로 저장됩니다.
Calculator.prototype.add; //  [Function:add]
Calculator.prototype.subtract; //  [Function:subtract]

객체 리터럴의 문법과 마찬가지로, 임의의 표현식을 대괄호로 둘러싸서 메소드의 이름으로 사용할 수도 있습니다.

const methodName = "introduce";
class Person {
  constructor({ name, age }) {
    this.name = name;
    this.age = age;
  }
  // 아래 메소드의 이름은 `introduce`가 됩니다.
  [methodName]() {
    return `안녕하세요, 제 이름은 ${this.name}입니다.`;
  }
}

console.log(new Person({ name: "윤아준", age: 19 }).introduce()); // 안녕하세요, 제 이름은 윤아준입니다.

Getter 혹은 setter 를 정의하고 싶을 때는 메소드 이름 앞에 get 또는 set 을 붙여주면 됩니다.

class Account {
  constructor() {
    this._balance = 0;
  }
  get balance() {
    return this._balance;
  }
  set balance(newBalance) {
    this._balance = newBalance;
  }
}

const account = new Account();
account.balance = 10000; //  set을 붙였으므로 값을 대입하거나 재대입할 수 있습니다.
account.balance; // 10000 : get의 역할대로 this._balance를 리턴합니다.

static 키워드를 메소드 이름 앞에 붙여주면 해당 메소드는 정적 메소드가 됩니다.

class Person {
  constructor({ name, age }) {
    this.name = name;
    this.age = age;
  }
  // 이 메소드는 정적 메소드입니다.
  static sumAge(...people) {
    return people.reduce((acc, person) => acc + person.age, 0);
  }
}

const person1 = new Person({ name: "윤아준", age: 19 });
const person2 = new Person({ name: "신하경", age: 20 });

Person.sumAge(person1, person2); // 39

[정적 메소드와 인스턴스 메소드 차이점]

// 정적 메소드 예시
class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  static distance(a, b) {
    const dx = a.x - b.x;
    const dy = a.y - b.y;
    return Math.sqrt(dx * dx + dy * dy);
  }
}

const p1 = new Point(5, 5);
const p2 = new Point(10, 10);

// 정적 메소드를 쓰기 위해 Point라는 생성자에 .을 찍고 distance라는 메소드를 사용하였습니다.
console.log(Point.distance(p1, p2));

// 일반 메소드 예시
class Polygon {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }

  get area() {
    return this.calcArea();
  }

  calcArea() {
    return this.height * this.width;
  }
}

const c = new Polygon(200, 300);
// 일반 메소드를 쓰기 위해 인스턴스에 .을 찍고 사용하였습니다.
c.calcArea();

Generator 메소드를 정의하려면, 메소드 이름 앞에 * 기호를 붙여주면 됩니다.
아래와 같이 Symbol.iterator 메소드를 generator 로 정의해주면, 클래스의 인스턴스를 쉽게 iterable 로 만들 수 있습니다.

class Gen {
  *[Symbol.iterator]() {
    yield 1;
    yield 2;
    yield 3;
  }
}

// 1, 2, 3이 차례대로 출력됩니다.
for (let n of new Gen()) {
  console.log(n);
}



클래스 필드 (Class Field)

클래스 블록 안에서 할당 연산자(=)를 이용해 인스턴스 속성을 지정할 수 있는 문법을 클래스 필드(class field)라고 합니다.

class Counter {
  static initial = 0; // static class field
  count = Counter.initial; // class field
  inc() {
    return this.count++;
  }
}

const counter = new Counter();
console.log(counter.inc()); // 0
console.log(counter.inc()); // 1

Counter.initial = 10;
console.log(new Counter().count); // 10

console.log(counter.inc()); // 2

클래스 필드는 아직 정식 표준으로 채택된 기능은 아닙니다. 아직 이 기능을 구현한 브라우저는 없는 상태이고,
Babel, TypeScript 등의 트랜스파일러를 통해 일부 기능을 사용할 수 있습니다.


클래스 필드와 this

class 블록은 새로운 블록 스코프를 형성하고, 이 내부에서 사용된 this 는 인스턴스 객체를 가리키게 됩니다.

class MyClass {
  a = 1;
  b = this.a;
}

new MyClass().b; // 1

이 성질을 이용하면, 화살표 함수를 통해서 메소드를 정의할 수 있습니다.
(화살표 함수 안에서의 this 키워드는 바로 바깥쪽 스코프에 존재하는 this 와 같은 객체를 가리킨다는 사실을 떠올려보세요.)

class MyClass {
  a = 1;
  getA = () => {
    return this.a;
  };
}

new MyClass().getA(); // 1

이렇게만 보면 일반적인 메소드와 별로 차이가 없어 보이지만, 사실 동작방식 측면에서 굉장히 큰 차이점이 있습니다.

  1. 일반적인 메소드는 클래스의 prototype 속성에 저장되는 반면, 클래스 필드는 인스턴스 객체에 저장됩니다.
  2. 화살표 함수의 this 는 호출 형태에 관계없이 항상 인스턴스 객체를 가리키게 됩니다.


2 번 성질때문에, 메소드를 값으로 다루어야 할 경우에는 일반적인 메소드 대신 화살표 함수가 사용되는 경우가 종종 있습니다.
다만, 일반적인 메소드와 달리, 클래스 필드를 통해 정의한 메소드는 인스턴스를 생성할 때마다
새로 생성되기 때문에 메모리를 더 차지하게 되므로 주의해서 사용해야 합니다.

class MyClass {
  constructor() {
    this.a = 1;
    this.getA = () => {
      return this.a;
    };
  }

  _getA() {
    return this.a;
  }
}

const obj1 = new MyClass();
const obj2 = new MyClass();
obj1._getA === obj2._getA; // true - _getA가 MyClass의 프로토타입이므로 같은 것을 가져다쓰는 것으로 같습니다.
obj1.getA === obj2.getA; // false - 화살표 함수가 새로운 객체를 만들어서 값은 같아보여도 참조값이므로 본질적으로는 다릅니다.



클래스 상속 (Class Inheritance)

클래스 상속(class inheritance, subclassing) 기능을 통해 한 클래스의 기능을 다른 클래스에서 재사용할 수 있습니다.

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

  speak() {
    console.log(this.name + " makes a noise");
  }
}

class Dog extends Animal {
  speak() {
    console.log(this.name + " bark.");
  }
}

let d = new Dog("cookie");
d.speak();

위 코드에서, extends 키워드를 통해 Dog 클래스가 Animal 클래스를 상속했습니다.
이 관계를 보고 ‘부모 클래스-자식 클래스 관계’ 혹은 ‘슈퍼 클래스(superclass)-서브 클래스(subclass) 관계’라고 말하기도 합니다.

어떤 클래스 A 가 다른 클래스 B 를 상속받으면, 다음과 같은 일들이 가능해집니다.

class Parent {
  static staticProp = "staticProp";
  static staticMethod() {
    return "I'm a static method.";
  }
  instanceProp = "instanceProp";
  instanceMethod() {
    return "I'm a instance method.";
  }
}

class Child extends Parent {}

console.log(Child.staticProp); // staticProp
console.log(Child.staticMethod()); // I'm a static method.

const c = new Child();
console.log(c.instanceProp); // instanceProp
console.log(c.instanceMethod()); // I'm a instance method.


super

앞서 봤듯이, 자식 클래스에서 부모 클래스의 정적 속성과 인스턴스 속성에 접근할 수 있었습니다.
하지만, 자식 클래스에 같은 이름의 속성을 정의한 경우 문제가 생깁니다.

class Melon {
  getColor() {
    return "제 색깔은 초록색입니다.";
  }
}

class WaterMelon extends Melon {
  getColor() {
    return "속은 빨강색입니다.";
  }
}

const waterMelon = new WaterMelon();
waterMelon.getColor(); // 속은 빨강색입니다.

이런 경우에, super 키워드를 통해 부모 클래스의 속성에 직접 접근할 수 있습니다.

class Melon {
  getColor() {
    return "제 색깔은 초록색입니다.";
  }
}

class WaterMelon extends Melon {
  getColor() {
    return super.getColor() + " 하지만 속은 빨강색입니다."; // super를 통해 부모인 Melon의 getColor를 가져와서 출력
  }
}

const waterMelon = new WaterMelon();
waterMelon.getColor(); // 제 색깔은 초록색입니다. 하지만 속은 빨강색입니다.

super 키워드의 동작 방식은 다음과 같습니다.

class Person {
  constructor({ name, age }) {
    this.name = name;
    this.age = age;
  }
  introduce() {
    return `제 이름은 ${this.name}입니다.`;
  }
}

class Student extends Person {
  constructor({ grade, ...rest }) {
    // 부모 클래스의 생성자를 호출할 수 있습니다.
    super(rest);
    this.grade = grade;
  }
  introduce() {
    // 부모 클래스의 `introduce` 메소드를 호출할 수 있습니다.
    return super.introduce() + ` 저는 ${this.grade}학년입니다.`;
  }
}

const s = new Student({ grade: 3, name: "윤아준", age: 19 });
s.introduce(); // 제 이름은 윤아준입니다. 저는 3학년입니다.



클래스 상속과 프로토타입 상속

클래스 상속은 내부적으로 프로토타입 상속 기능을 활용하고 있습니다.
아래 코드의 클래스 상속에 대한 프로토타입 체인을 그림으로 나타내보면 다음과 같이 됩니다.

class Person {}
class Student extends {}
const student = new Student();



2. Today I Found Out

자바에만 class가 있는 줄 알았는데 JS에서도 class가 있어서 신기했습니다.
클래스가 왜 생겼을까라는 의문을 가지고 공부를 시작했는데
공부를 하면서 생성자를 이용할때보다 클래스를 써서 생성하는 것이
더 이해가 수월했고, 코드가 간결해진 것 같아서 좋았습니다.
또한 상속이라는 부분이 정말 리액트에 점점 다가가고 있는 것같아서
너무 좋았습니다.
아직 얇게 봐서 조금 더 공부를 해야겠습니다.



3. refer

http://poiemaweb.com/es6-class

https://helloworldjavascript.net/pages/270-class.html#fn_1