자바스크립트/개념

자바스크립트 클래스 간단 정리

Jake Seo 2022. 7. 30. 17:05

개요

클래스는 오브젝트를 만들기 위한 템플릿이다. 데이터와 데이터를 조작하는 코드를 하나로 추상화한다. JS 클래스는 프로토타입을 기반으로 빌드되었지만 ES5 의 클래스 같은 의미와는 다른 문법과 의미를 가진다.

클래스 정의

JS 의 '특별한 함수'이다. JS 에서는 모든 것이 오브젝트로 표현 가능하다는 점을 염두에 두자.

구조

클래스 문법에는 2가지 구성 요소가 있다.

클래스 선언 (Class Declaration) 알아보기

class Rectangle {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
}

호이스팅

const p = new Rectangle(); // ReferenceError

class Rectangle {
  // ...
}

클래스 선언과 함수 선언의 차이는 함수는 선언된 곳 이전부터 사용이 가능한데, 클래스는 선언 코드가 작동한 이후에 new 키워드를 이용해 생성되어야 한다는 것이다. 위의 코드는 ReferenceError 가 나는 예제이다.

호이스팅의 기본 원리를 알고 있다면 어렵지 않게 이해할 수 있는 부분이다.

클래스 표현식 (Class Expression)

클래스 표현식은 namedunnamed로 나뉜다.

자바스크립트에서 표현식 이라는 개념은 보통 변수에 특정한 표현을 통해 해당 객체를 넣는 것을 말한다. 비슷한 예로 함수 표현식 이 있다.

// unnamed
let Rectangle = class {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
};
console.log(Rectangle.name);
// output: "Rectangle"

// named
Rectangle = class Rectangle2 {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
};
console.log(Rectangle.name);
// output: "Rectangle2"

namedunnamed 의 차이는 클래스 내부의 name 프로퍼티에 값을 변수명과 다르게 입력할 것인지 정도의 차이가 있다.

클래스 바디와 메서드 정의 알아보기

클래스 바디는 {} 내부에 들어간다. 내부에 클래스 멤버인 메서드, 생성자를 정의한다.

Strict mode

클래스 바디는 더 나은 성능을 제공하기 위해 Strict mode 에서 실행된다. 문법을 따르지 않으면 silent errors 가 던져질 것이다. 그리고 몇몇 키워드는 미래의 ECMA Script 를 위해 예약되어 있다.

Constructor

생성자(constructor) 메서드는 class 로 생성된 객체를 생성하고 초기화하기 위한 특수한 메서드이다. 클래스 안에 한개만 존재해야 하고, 그 이상 존재한다면 SyntaxError 가 발생한다.

생성자 내부에서는 super 키워드를 통해 부모의 생성자를 불러오는 것도 가능하다.

정적 초기화 블럭 (Static initialize blocks)

정적 초기화 블럭클래스 정적 프로퍼티 를 유연하게 초기화할 수 있게 해준다. 초기화 동안의 구문의 평가와 private scope 접근 권한까지 포함한다.

여러 개의 정적 초기화 블럭이 선언될 수 있으며, 정적 프로퍼티 메서드와 함께 교차되는 것도 가능하다. 모든 정적 아이템은 선언 순서대로 평가된다.

프로토타입 메서드

class Rectangle {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }

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

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

const square = new Rectangle(10, 10);

console.log(square.area); // 100

제너레이터 메서드

Iterators and generators 페이지에서도 볼 수 있다.

class Polygon {
  constructor(...sides) {
    this.sides = sides;
  }

  // Method
  *getSides() {
    for (const side of this.sides) {
      yield side;
    }
  }
}

const pentagon = new Polygon(1, 2, 3, 4, 5);

console.log([...pentagon.getSides()]); // [1, 2, 3, 4, 5]

정적 메서드와 프로퍼티

static 키워드로 선언된 메서드는 인스턴스에서는 사용할 수 없고 클래스에서 유틸 메서드처럼 접근해 사용하는 것이 권장된다. 캐시, 고정 환경 설정 혹은 인스턴스 생성 시마다 매번 만들면 중복되는 데이터 등을 관리할 수 있다.

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  static displayName = "Point";
  static distance(a, b) {
    const dx = a.x - b.x;
    const dy = a.y - b.y;

    return Math.hypot(dx, dy);
  }
}

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

p1.displayName; // undefined;
p2.displayName; // undefined; -> 인스턴스에서 접근 불가

p1.distance; // undefined;
p2.distance; // undefined; -> 인스턴스에서 접근 불가

console.log(Point.displayName); // "Point" -> 클래스에서 접근 가능
console.log(Point.distance(p1, p2)); // 7.0710678118654755

프로토타입과 정적메서드와 함께 this 바인딩하기

this 의 값을 클래스 바디에서 불러오게 되면, 기본적으로 undefined 가 반환된다. 그 이유는 클래스 바디에서 "use strict" 가 이미 적용되어 있기 때문이다. 아래의 예제코드를 콘솔에서 실행해보면 쉽게 알 수 있다. this 의 바인딩은 strict 인지 아닌지에 따라 달라진다.

function strict() {
  "use strict";
  console.log(this);
}

function nonStrict() {
  console.log(this);
}
class Animal {
  speak() {
    return this;
  }
  static eat() {
    return this;
  }
}

const obj = new Animal();
obj.speak(); // the Animal object
const speak = obj.speak;
speak(); // undefined

Animal.eat(); // class Animal
const eat = Animal.eat;
eat(); // undefined

아래와 같이 non-strict 모드를 사용하도록 하는 방식으로 prototype 이나 static method 를 설정하면 this 를 바인딩할 수 있다.

function Animal() {}

Animal.prototype.speak = function () {
  return this;
};

Animal.eat = function () {
  return this;
};

const obj = new Animal();
const speak = obj.speak;
speak(); // global object (in non–strict mode)

const eat = Animal.eat;
eat(); // global object (in non-strict mode)

인스턴스 프로퍼티

인스턴스 프로퍼티는 무조건 클래스 메서드 안에서 정의되어야 한다.

class Rectangle {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
}

필드 선언

퍼블릭 필드 선언

  • let, const, var 키워드는 필요 없다.
  • 맨 위쪽(up-front)에 먼저 선언해둬야 self-documenting 의 효과를 볼 수 있다.
  • 아래의 예제처럼 기본 값을 설정해둘 수도 있다.

퍼블릭 필드 관련 공식문서

class Rectangle {
  height = 0;
  width;
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
}

프라이빗 필드 선언

  • 클래스 밖에서 프라이빗 필드를 이용하면 참조 에러가 발생한다.
  • 클래스 바디에서만 읽거나 쓰여질 수 있다.
  • 프라이빗 필드는 일반 필드처럼 나중에 프로퍼티를 이용하여 만들어낼 수 없다.
  • 클래스 내부에서 구성하였으므로 이용자는 버전에 따라 바뀔 수 있는 내부 구현에 의존하지 않는다는 것을 확신할 수 있다.

프라이빗 필드 관련 공식문서

class Rectangle {
  #height = 0;
  #width;
  constructor(height, width) {
    this.#height = height;
    this.#width = width;
  }
}

extends 키워드를 이용한 클래스 상속 (Sub classing)

  • 클래스를 다른 클래스의 자식으로 만들기 위해 클래스 선언, 혹은 클래스 표현식에서 사용된다.
  • 생성자에서 super() 메서드를 이용해 부모의 생성자를 호출해야 한다.

클래스 상속 예제

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

  speak() {
    console.log(`${this.name} makes a noise.`);
  }
}

class Dog extends Animal {
  constructor(name) {
    super(name); // call the super class constructor and pass in the name parameter
  }

  speak() {
    console.log(`${this.name} barks.`);
  }
}

const d = new Dog("Mitzie");
d.speak(); // Mitzie barks.

함수를 기반으로 한 클래스 상속 예제

위와 비슷하나,

  • 부모 클래스를 생성할 때 prototype 으로 공통 메서드를 지정한다.
  • constructor 키워드를 이용해 생성자를 만들지 않는다.
function Animal(name) {
  this.name = name;
}

Animal.prototype.speak = function () {
  console.log(`${this.name} makes a noise.`);
};

class Dog extends Animal {
  speak() {
    console.log(`${this.name} barks.`);
  }
}

const d = new Dog("Mitzie");
d.speak(); // Mitzie barks.

// For similar methods, the child's method takes precedence over parent's method

생성자가 없는 일반 함수를 상속하기

생성자가 없는 일반 함수를 상속할 때는 Object.setPrototypeOf() 메서드를 이용하면 가능하다.

const Animal = {
  speak() {
    console.log(`${this.name} makes a noise.`);
  },
};

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

// If you do not do this you will get a TypeError when you invoke speak
Object.setPrototypeOf(Dog.prototype, Animal);

const d = new Dog("Mitzie");
d.speak(); // Mitzie makes a noise.

Species

직접 만든 클래스에서 Array 를 상속받아 기본 생성자가 실행된 이후에는 Array 타입을 반환할 수 있다.

class MyArray extends Array {
  // 부모 Array 생성자로 species 덮어쓰기
  static get [Symbol.species]() {
    return Array;
  }
}
var a = new MyArray(1, 2, 3);
var mapped = a.map((x) => x * x);

console.log(mapped instanceof MyArray); // false
console.log(mapped instanceof Array); // true

static get [Symbol.species]() { return Array; } 코드가 없었다면, 둘 다 true 가 나왔을 것이다.

super 를 통한 상위 클래스 호출

객체의 부모가 가진 메서드를 호출할 수 있다. 이는 프로토타입 기반 상속에 대비되는 장점 중 하나이다. 부모의 메서드와 자신의 메서드 중 선택할 수 있다.

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

  speak() {
    console.log(`${this.name} makes a noise.`);
  }
}

class Lion extends Cat {
  speak() {
    super.speak();
    console.log(`${this.name} roars.`);
  }
}

let l = new Lion("Fuzzy");
l.speak();
// Fuzzy makes a noise.
// Fuzzy roars.

믹스인 (Mix-ins)

추상 서브 클래스 혹은 믹스인이라고 불리는 이것은 클래스를 위한 템플릿이다. ECMAScript 클래스는 하나의 상위 클래스만 가질 수 있다.

슈퍼클래스를 인자로 받고 이 슈퍼클래스를 확장하는 서브 클래스를 생성하여 반환하는 함수를 구현하는 방식으로 ECMAScript 에서 믹스인(Mix-ins)을 구현할 수 있다.

var calculatorMixin = (Base) =>
  class extends Base {
    calc() {}
  };

var randomizerMixin = (Base) =>
  class extends Base {
    randomize() {}
  };

위 코드의 Base 에 클래스를 넣으면 된다.

class Foo {}
class Bar extends calculatorMixin(randomizerMixin(Foo)) {}

클래스 재정의

클래스 재정의는 불가능하며, 재정의를 시도하면 SyntaxError 가 발생한다.

레퍼런스

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes

반응형