# Chapter 9. 클래스

허상범·2023년 2월 6일
0
post-thumbnail

자바스크립트 완벽 가이드 7판 스터디
책의 목차를 따라가며 관련 내용을 학습하고 공유하는 스터디입니다.

1. JavaScript Class
2. 생성자 함수를 클래스로 변경해보기
3. 클래스의 메서드: constructor, prototype method, static method
4. 클래스의 프로퍼티: instance property
5. 클래스 필드: private, static
6. 클래스의 getter, setter
7. 상속(확장): extends, super, overriding
8. 예제 연습


1. JavaScript Class


2. 생성자 함수를 클래스로 변경해보기

생성자 함수로 작성한 코드

// 대문자 사용
function Range(from, to) {
  if (!new.target) console.error('Range가 생성자로 호출되지 않음');

  //인스턴스 프로퍼티 : 상속되지않는 인스턴스 고유의 값
  this.from = from;
  this.to = to;
}

// 프로토타입 설정
// Range를 생성자로 호출 시 자동으로 Range.prototype을 새 Range 객체의 프로토타입으로 사용
Range.prototype = {
  // 역참조. 새로 생성한 객체로 덮어썼기 때문에 constructor를 작성 필요
  // constructor는 생성자 함수 자체를 참조함
  constructor: Range,

  // 프로토타입 메서드
  includes: function (x) {
    return this.from <= x && x <= this.to;
  },
  [Symbol.iterator]: function* () {
    for (let x = Math.ceil(this.from); x <= this.to; x++) {
      yield x;
    }
  },
  // 화살표 함수로 메서드를 작성하는 것은 무의미
  //
  toString: () => {
    console.log(this); // Window, 호출한 객체가 아니라 자신이 정의된 컨텍스트에서 this를 상속받는다.
    return `(${this.from} ... ${this.to})`;
  },
};

// new 사용
const oneToTen = new Range(1, 10);
const tenTotwenty = new Range(10, 20);

// Range 생성자함수
console.dir(Range);

// Range 생성자함수를 사용해 만든 인스턴스
console.dir(oneToTen);
console.dir(tenTotwenty);

console.log(oneToTen instanceof Range); // true
console.log(tenTotwenty instanceof Range); // true

console.log(Range.prototype.isPrototypeOf(oneToTen)); // true
console.log(Range.prototype.isPrototypeOf(tenTotwenty)); // true
console.log(oneToTen.prototype === tenTotwenty.prototype); // true

Class 문법으로 작성한 코드

// class 선언의 바디는 묵시적으로 스트릭트 모드로 동작

class Range {
  // 생성자 함수 정의
  // 새 변수 Range에 constructor 함수의 값을 할당
  constructor(from, to) {
    this.from = from;
    this.to = to;
  }

  // function 키워드 생략
  // 콤마로 구분 x
  // key:value로 정의하지 않음
  includes(x) {
    return this.from <= x && x <= this.to;
  }

  *[Symbol.iterator]() {
    for (let x = Math.ceil(this.from); x <= this.to; x++) {
      yield x;
    }
  }

  toString() {
    console.log(this);
    return `(${this.from} ... ${this.to})`;
  }
}
// new 사용
const oneToTen = new Range(1, 10);
const tenTotwenty = new Range(10, 20);

// Class Range
console.dir(Range);

// Range 클래스로 만든 인스턴스
console.dir(oneToTen);
console.dir(tenTotwenty);

console.log(oneToTen instanceof Range); // true
console.log(tenTotwenty instanceof Range); // true

console.log(Range.prototype.isPrototypeOf(oneToTen)); // true
console.log(Range.prototype.isPrototypeOf(tenTotwenty)); // true
console.log(oneToTen.prototype === tenTotwenty.prototype); // true

위의 생성자 함수, 클래스 코드를 실행한 결과와 프로토타입 도식화



3. 클래스의 메서드: constructor, prototype method, static method

constructor

인스턴스를 생성하고 인스턴스 프로퍼티를 초기화하는 역할

  • 인스턴스 프로퍼티는 constructor의 인자로 받거나 내부에서 값을 할당한다.
class Car {
  // 이 시점에 constructor가 객체(인스턴스)를 생성하고,
  // this가 그 객체(인스턴스)를 참조한다.
  // 그렇기 때문에 constructor가 제일 상단에 있어야 한다.
  constructor(name, manufacturer) {
    this.name = name;
    this.manufacturer = manufacturer; // 인자로 받음
    this.id = 1; // 내부에서 할당
  }

  display() {
    console.log(`${this.name} of ${this.manufacturer}`);
  }
}

생략할 수 있다.

  • 생략하면 암묵적으로 빈 constructor가 정의된고 빈 객체를 생성한다.
class Car {
  // constructor() {}
  constructor() {}
  // 작성하지 않아도 이렇게 임의의 빈 constructor가 정의된다.
  // 빈 객체를 생성하고 this는 그 빈 객체를 참조한다.

  go() {
    this; // 생성된 인스턴스 객체
    console.log(`gogogo`);
  }
}

return 문을 갖지 않아야 한다.

  • 원시값을 반환하면 무시된다.
  • 객체를 반환하면 인스턴스 대신 반환한 객체가 생성된다.
class Car {
  constructor(name, manufacturer) {
    this.name = name;
    this.manufacturer = manufacturer;

    return 'car';
  }

  display() {
    console.log(`${this.name} of ${this.manufacturer}`);
  }
}

class Truck {
  constructor(name, manufacturer) {
    this.name = name;
    this.manufacturer = manufacturer;

    return { length: 100 };
  }

  display() {
    console.log(`${this.name} of ${this.manufacturer}`);
  }
}

const myRaptor = new Truck('Raptor', 'Ford');
console.log(myRaptor); // {length: 100}

클래스의 constructor 메서드와 프로토타입의 constructor 다르다.


prototype method

  • 클래스 문법은, 생성자 함수를 사용했을 때와 다르게 직접 prototype 프로퍼티에 추가하지 않아도 기본적으로 prototype method로 추가된다.
  • 메소드를 화살표 함수로 작성하게되면 prototype method 가 아닌 instance method 가 되니 주의한다.
    • 이 경우 this 바인딩도 문제지만, 인스턴스를 생성할 때마다 메소드가 새롭게 생성되 비효율적이다.
class Range {
  constructor(from, to) {
    this.from = from;
    this.to = to;
  }

  includes(x) {
    return this.from <= x && x <= this.to;
  }
  *[Symbol.iterator]() {
    for (let x = Math.ceil(this.from); x <= this.to; x++) {
      yield x;
    }
  }
  toString() {
    console.log(this);
    return `(${this.from} ... ${this.to})`;
  }
}

const oneToTen = new Range(1, 10);
const tenToTwenty = new Range(1, 10);

console.dir(oneToTen);
console.dir(tenToTwenty);

console.log(oneToTen instanceof Range);
console.log(tenToTwenty instanceof Range);

console.log(Range.prototype.isPrototypeOf(oneToTen));
console.log(Range.prototype.isPrototypeOf(tenToTwenty));
console.log(oneToTen.prototype === tenToTwenty.prototype);

static method

  • 메서드 앞에 static 을 붙이면 정적 메서드가 된다.
  • 프로토타입 프로퍼티가 아니라 클래스(생성자 함수)의 프로퍼티로 정의된다.
  • 인스턴스를 생성하지 않고 사용할 수 있다.
    • 클래스의 정의가 평가되는 시점에 클래스 자체가 함수 객체가 된다.
    • 따라서 인스턴스를 생성하지 않고도 내부 메서드를 가질 수 있다.
  • 인스턴스에서 호출할 수 없다.
class Range {
  constructor(from, to) {
    this.from = from;
    this.to = to;
  }

  static maker() {
    return 'sangbeomheo';
  }

  includes(x) {
    return this.from <= x && x <= this.to;
  }

  toString() {
    console.log(this);
    return `(${this.from} ... ${this.to})`;
  }
}

const oneToTen = new Range(1, 10);
console.log(Range.maker()); // 'sangbeomheo'
console.log(oneToTen.maker()); // TypeError: oneToTen.whose is not a function
  • 표준 빌트인 객체의 정적 메서드 (네임스페이스 효과)
Math.max();
Math.min();
Object.keys();
Array.from();
Array.isArray();
등등;

4. 클래스의 프로퍼티: instance property

  • 이전에는 생성자함수 내부에서만 작성이 가능했지만 현재는 필드 기능으로 생성자함수 밖에서도 작성이 가능하다.
  • this[propertyName] 으로 접근한다.
class Range {
  constructor(from, to) {
    // 인스턴스 프로퍼티 초기화
    // 만들어진 빈 객체를 this가 가리키고, 그 this에 프로퍼티를 추가한다.
    this.from = from;
    this.to = to;
  }

  toString() {
    console.log(this);
    return `(${this.from} ... ${this.to})`;
  }
}

5. 클래스 필드: public, private(#), static

클래스 필드?

  • 클래스 기반 객체지향 언어에서 클래스가 생성한 인스턴스의 프로퍼티를 가리킨다.
  • 자바스크립트의 경우 인스턴스의 프로퍼티를 선언/초기화하려면 constructor를 반드시 사용해야 한다.
  • 하지만 클래스 필드가 동작한다면 자바스크립트의 클래스 바디에 this 없이 선언할 수 있다.
  • 현재 Stage 4, https://github.com/tc39/proposal-class-fields
    • "class definitions become more self-documenting";
  • 따라서 외부 값으로 클래스 필드를 초기화해야한다면 constructor를 사용하고,
  • 외부 값으로 초기화할 필요가 없다면 클래스 필드로 정의하는 것도 가능하다.
class Person {
  name = 'Sangbeom'; // 클래스 필드 선언. 앞에 아무것도 없이 선언하면 public이다.

  constructor(age) {
    this.age = 19;
  }
}

new Person(); // Person {age: 19, name: "Sangbeom"}

private

  • 캡슐화. #을 붙여 사용. 외부에서 접근이 불가능하다.
  • 클래스 내부에서만 참조할 수 있다.
  • 클래스 바디에 정의하지 않으면 에러가 난다.

static

  • static을 붙여 사용
  • 정적 메서드에 추가로 정적 필드를 정의할 수 있다.
  • 인스턴스가 아닌 클래스의 프로퍼티가 된다.
  • private으로 캡슐화가 가능하다.
class Car {
  constructor(name, manufacturer) {
    this.name = name;
    this.manufacturer = manufacturer;
  }
  static #count = 1; // static, private

  // static method
  // 이름과 제조사를 랜덤으로 받는 새로운 인스턴스 객체를 리턴하는 정적메소드
  static makeRandomCar() {
    const [name, manufacturer] = [
      `RandomName${this.#count}`, // 클래스 내부에서 접근 가능
      `RandomManufacturer${this.#count}`,
    ];
    this.#count++;

    return new Car(name, manufacturer);
  }

  display() {
    console.log(`${this.name} of ${this.manufacturer}`);
  }
}

const myModel3 = new Car('Model3', 'Tesla');
const myRapter = new Car('Rapter', 'Ford');
const random1 = Car.makeRandomCar();
const random2 = Car.makeRandomCar();

console.log(random1); // Car {name: 'RandomName1', manufacturer: 'RandomManufacturer1'}
console.log(random2); // Car {name: 'RandomName2', manufacturer: 'RandomManufacturer2'}
console.log(Car.#count); // SyntaxError: Private field '#count' must be declared in an enclosing class

console.log(random2.display()); // RandomName2 of RandomManufacturer2
console.log(random2.makeRandomCar()); // Uncaught TypeError: random2.makeRandomCar is not a function

6. 클래스의 getter, setter

class Student {
  #firstName;
  #lastName;
  constructor(firstName, lastName) {
    this.#firstName = firstName;
    this.#lastName = lastName;
  }
  get fullName() {
    return `${this.#lastName} ${this.#firstName}`;
  }

  set fullName(value) {
    console.log('set', value);
  }
}

const student = new Student('상범', '허');
student.#firstName = '종승'; // Uncaught SyntaxError: Private field '#firstName' must be declared in an enclosing class
console.log(student.#firstName); // // Uncaught SyntaxError: Private field '#firstName' must be declared in an enclosing class
console.log(student.fullName); // 허 상범
student.fullName = '박상은'; // set 박상은
console.log(student.fullName); // 허 상범

7. 상속(확장): extends, super, overriding

  • 클래스는 기존의 클래스를 상속 받아 새로운 클래스로 확장할 수 있다.
  • 수퍼클래스 & 서브클래스(상속을 통해 확장된 클래스)
  • 수퍼클래스에서 공통의 속성들 부여하고, 각각의 서브클래스에서 개별 속성을 부여할 수 있다.
    • 코드 효율성 높아짐

extends

  • 클래스 확장을 위한 키워드
  • 인스턴스의 프로토타입, 클래스 간의 프로토타입도 연결된다.
    • 프로토타입 메서드, 정적 메서드 모두 상속이 가능하다.
class Animal {
  constructor(name, weight) {
    this.name = name;
    this.weight = weight;
  }
  static testStatic() {
    console.log('static');
  }

  eat() {
    console.log(`${this.name}이 먹는다.`);
  }
  sleep() {
    console.log(`${this.name}이 잔다.`);
  }
}

class Dog extends Animal {} // 확장

const dog = new Dog('a', 300);
console.dir(dog);
dog.sleep(); // a이 잔다.
dog.eat(); // a이 먹는다.
Dog.testStatic(); // 'static'

'super'

  • 서브클래스는 직접 인스턴스를 생성하지 않고 수퍼클래스에게 인스턴스 생성을 위임한다.
    • 서브클래스의 constructor에서 반드시 super를 호출해야한다.
  • super를 호출하면 수퍼클래스의 constructor를 호출한다.
  • super를 호출하면 수퍼클래스의 메서드를 호출할 수 있다.
class Animal {
  constructor(name, weight) {
    this.name = name;
    this.weight = weight;
  }
  static testStatic() {
    console.log('static');
  }

  eat() {
    console.log(`${this.name}이 먹는다.`);
  }
  sleep() {
    console.log(`${this.name}이 잔다.`);
  }
}

class Cat extends Animal {
  constructor(name, weight, space) {
    // 수퍼클래스의 constructor를 호출해서 인스턴스 객체를 생성한다.
    // 그렇기 때문에 수퍼클래스의 프로퍼티를 그대로 갖는다.
    // constructor를 생략하지 않는 경우 무조건 super를 호출해야 한다.
    super(name, weight);
    this.space = space;
  }

  eat() {
    // method overriding
    // 수퍼클래스의 프로토타입 메서르르 가리킨다.
    super.eat();
    console.log('고양이는 많이 먹는다');
  }
}

const cat = new Cat('b', 100, '러시안블루');
console.log(cat);
cat.eat(); // b이 먹는다. 고양이는 많이 먹는다
cat.sleep(); // b이 잔다.
Cat.testStatic(); // static (정적 메소드도 상속)

8. 예제 연습

네임스페이스 사용하기

class Classifier {
  static classify(number) {
    return (
      `\n${number} : ` +
      (Classifier.isPerfect(number) ? 'perfect, ' : '') +
      (Classifier.isAbundant(number) ? 'abundant, ' : '') +
      (Classifier.isDeficient(number) ? 'deficient, ' : '') +
      (Classifier.isPrime(number) ? 'prime ' : '') +
      (Classifier.isSquared(number) ? 'squard ' : '')
    );
  }

  static isPerfect(number) {
    return this.sum(this.getFactors(number)) - number == number;
  }

  static isAbundant(number) {
    return this.sum(this.getFactors(number)) - number > number;
  }

  static isDeficient(number) {
    return this.sum(this.getFactors(number)) - number < number;
  }

  static isPrime(number) {
    return this.getFactors(number).length === 2;
  }

  static isSquared(number) {
    return this.getFactors(number).some(factor => Math.pow(factor, 2) === number);
  }

  static sum(factors) {
    return factors.reduce((total, curr) => total + curr, 0);
  }

  static getFactors(number) {
    return [...Array(number)]
      .map((v, i) => i + 1)
      .filter(potentialFactor => number % potentialFactor == 0);
  }
}
Object.freeze(Classifier); // 메소드를 변경하지 못하게 프리즈

console.log(Classifier.classify(17)); // 17 : deficient, prime
console.log(Classifier.classify(16)); // 16 : deficient, squard

팩토리클래스 만들기



참고자료

0개의 댓글