클래스는 객체를 생성하기 위한 템플릿으로 데이터와 이를 조작하는 코드를 하나로 추상화한다.
클래스는 사실 "특별한 함수"이다. 함수를 함수 표현식과 함수 선언으로 정의할 수 있듯이 클래스 문법도 클래스표현식과 클래스선언의 두 가지 방법을 제공한다.
클래스를 선언하기 위해서는 클래스의 이름과 함께 class 키워드를 사용한다.
class Rectangle {
constructor(height, width) {
this.height = height;
this.width = width;
}
}
클래스 표현식은 이름을 가질 수도 있고, 갖지 않을 수도 있다. 이름을 가진 클래스 표현식의 이름은 클래스 몸체의 local scope 내부에서만 유효하다.
(but, 클래스의 name 속성을 통해 찾을 수 있다).
// unnamed 클래스
let Rectangle = class {
constructor(height, width) {
this.height = height;
this.width = width;
}
};
console.log(Rectangle.name); // "Rectangle"
// named 클래스
let Rectangle = class Rectangle2 {
constructor(height, width) {
this.height = height;
this.width = width;
}
};
console.log(Rectangle.name); // "Rectangle2"
함수 선언과 클래스 선언의 중요한 차이점은 함수의 경우 정의하기 하기 전에 호출할 수 있지만, 클래스는 반드시 정의한 뒤에 사용할 수 있다는 점이다.
const p = new Rectangle(); // ReferenceError
class Rectangle {}
위와 같이 참조에러가 발생하는 이유는 클래스가 호이스팅 될 때 초기화는 되지 않기 때문이다.
클래스는 let,const 키워드로 선언한 변수처럼 호이스팅 되기 때문에 클래스 선언문 이전에는 일시적 사각지대에 빠진다.
클래스는 생성자 함수이며 new연산자와 함께 호출되어 인스턴스를 생성한다.
함수는 new 연산자의 사용 여부에 따라 일반 함수로 호출되거나 생성자 함수로 호출되지만 클래스는 인스턴스를 생성하는 것이 유일한 존재 이유이므로 반드시 new 연산자와 함께 호출해야 한다.
클래스 표현식으로 정의된 클래스의 경우 다음 예제와 같이 클래스를 가리키는 식별자를 사용해 인스턴스를 생성하지 않고 기명 클래스 표현식의 클래스 이름(MyClass)을 사용해 인스턴스를 생성하면 에러가 발생한다.
const Person = class Myclass{};
const me = new Person();
console.log(Person); // ReferenceError : MyClass is not defined
이는 기명함수 표현식과 마찬가지로 클래스 표현식에서 사용한 클래스 이름은 외부코드에서 접근이 불가능하기 때문이다.
클래스로 생성된 객체를 생성하고 초기화하기 위한 특수한 메서드다. "constructor" 라는 이름을 가진 특수한 메서드는 클래스 안에 한 개만 존재할 수 있고 생략할 수도 있다.
class Person{
constructor(){
this.name = 'Kim';
this.age = 30;
}
}
const me = newPerson();
console.log(me) // Person { name:"Kim" , age:30 }
생성자는 별도의 반환문을 갖지 않아야 한다.
생성자는 부모 클래스의 생성자를 호출하기 위해 super 키워드를 사용할 수 있다.
class Rectangle {
constructor(height, width) {
this.height = height;
this.width = width;
}
// 프로토타입 메서드
calcArea() {
return this.height * this.width;
}
}
const square = new Rectangle(10, 10);
console.log(square.calcArea()); // 100
인스턴스를 생성하지 않아도 호출할 수 있는 메서드로 클래스의 인스턴스에서는 호출할 수 없다.
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);
p1.displayName; // undefined
p1.distance; // undefined
console.log(Point.displayName); // "Point"
console.log(Point.distance(p1, p2)); // 7.07106781
정적 메서드는 어플리케이션을 위한 유틸리티 함수를 생성하는 데 주로 사용된다.
정적 메서드와 프로토타입 메서드의 차이
- 각각 속해 있는 프로토타입 체인이 다르다.
- 정적 메서드는 클래스로 호출하고 프로토타입 메서드는 인스턴스로 호출한다.
- 정적 메서드는 인스턴스 프로퍼티를 참조할 수 없지만 프로토타입 메서드는 인스턴스 프로퍼티를 참조할 수 있다.
클래스에서 정의한 메서드의 특징
- function 키워드를 생략한 메서드 축약 표현을 사용한다.
- 클래스에 여러 메서드를 정의할 때 콤마가 필요없다.
- 암묵적으로 strict mode로 실행된다.
- for..in문이나 Object.keys 메서드 등으로 열거할 수 없다.
- 내부 메서드 [[Construct]]를 갖지 않는다. 따라서 new 연산자와 함께 호출할 수 없다.
클래스 필드란 클래스가 생성할 인스턴스의 프로퍼티를 가리키는 용어이다.
자바의 클래스 필드는 마치 클래스 내부에서 변수처럼 사용된다.
최신 브라우저로 넘어오면서 클래스 필드를 클래스 몸체에 정의할 수 있게 되었다. 하지만 클래스 몸체에서 클래스 필드를 정의하는 경우 this에 클래스 필드를 바인딩 해서는 안 된다. this는 클래스의 constructor와 메서드 내에서만 유효하다.
class Rectangle {
height = 0; // 없어도 똑같다.
width; // 없어도 똑같다.
constructor(height, width) {
this.height = height;
this.width = width;
}
}
위 코드 처럼 인스턴스를 생성할 때 클래스 필드를 초기화할 필요가 있다면 어짜피 constructor 내부에서 초기값을 할당해야 하기 때문에 constructor 밖에서 클래스 필드를 정의할 필요가 없다.
class Rectangle {
#height = 0;
#width;
constructor(height, width) {
this.#height = height;
this.#width = width;
}
}
console.log(Rectangle.#height);
// Uncaught SyntaxError: Private field '#height' must be declared in an enclosing class
private 필드는 반드시 클래스 몸체에 정의해야 하며 클래스 내부에서만 읽고 쓰기가 가능하다.
클래스의 바깥에서 private 필드를 접근하려고 하면 에러가 발생한다.
이처럼 클래스 외부에서 private 필드에 직접 접근 할 수 있는 방법은 없다. 다만 접근자 프로퍼티를 통해 간접적으로 접근하는 방법은 유효하다.
class Rectangle {
#height = 0;
#width;
constructor(height, width) {
this.#height = height;
this.#width = width;
}
get height(){
return this.#height;
}
}
const R = new Rectangle(20);
console.log(R.height); //20
클래스 외부에서 보이지 않도록 정의하였으므로 클래스 내부 구현이 바뀌더라도 클래스 사용자 입장에서는 이에 아무런 영항을 받지 않도록 할 수 있다.
상속에 의한 클래스 확장은 기존 클래스를 상속받아 새로운 클래스를 확장하여 정의하는 것이다.
예를 들어, 동물을 추상화한 Animal 클래스와 새와 사자를 추상화한 Bird, Lion 클래스를 각각 정의한다고 생각해보자. 새와 사자는 동물에 속하므로 동물의 속성을 갖는다. 하지만 새와 사자는 자신만의 고유한 속성도 갖는다. 이때 Animal 클래스는 동물의 속성을 표현하고 Bird, Lion 클래스는 상속을 통해 Animal 클래스의 속성을 그대로 사용하면서 자신만의 고유한 속성만 추가하여 확장할 수 있다.
상속을 통해 Animal 클래스를 확장한 Bird 클래스를 구현해 보자.
class Animal {
constructor(age, weight) {
this.age = age;
this.weight = weight;
}
eat() {
return "eat";
}
move() {
return "move";
}
}
// 상속을 통해 Animal 클래스를 확장한 Bird 클래스
class Bird extends Animal {
fly() {
return "fly";
}
}
const bird = new Bird(1, 5);
console.log(bird); // Bird {age: 1, weight: 5}
console.log(bird instanceof Animal); // true (프로토타입 체인으로 얽혀있다.)
console.log(bird instanceof Object); // true (스코프의 최 상위에는 Object가 있다)
console.log(bird.eat()); // eat
console.log(bird.move()); // move
console.log(bird.fly()); // fly
extends 키워드의 역할은 수퍼클래스와 서브클래스 간의 상속 관계를 설정하는 것이다. 클래스도 프로토타입을 통해 상속 관계를 구현한다.
class Base {} // 수퍼(베이스/부모) 클래스
class Derived extends Base {} // 서브(파생/자식) 클래스
상속을 통해 확장된 클래스를 서브(자식)클래스라 부르고, 서브클래스에게 상속된 클래스를 수퍼(부모)클래스라 부른다.
extends 키워드는 클래스뿐만 아니라 생성자 함수를 상속받아 클래스를 확장할 수도 있다. 단, extends 키워드 앞에는 반드시 클래스가 와야 한다.
// 생성자 함수
function Base(a) {
this.a = a;
}
// 생성자 함수를 상속받는 서브클래스
class Dervied extends Base {}
const dervied = new Derived(1);
console.log(derived); // Derived {a: 1}
extends 키워드 다음에는 클래스뿐만이 아니라 [[Construct]] 내부 메서드를 갖는 함수 객체로 평가될 수 있는 모든 표현식을 사용할 수 있다. 이를 통해 동적으로 상속받을 대상을 결정할 수 있다.
super키워드는 함수처럼 호출할 수도 있고, this와 같이 식별자처럼 참조할 수 있는 특별한 키워드다.
다음 예제와 같이 수퍼클래스의 constructor 내부에서 추가한 프로퍼티를 그대로 갖는 인스턴스를 생성한다면 서브클래스의 constructor를 생략할 수 있다. 이때 new 연산자와 함께 서브클래스를 호출하면서 전달한 인수는 모두 서브클래스에 암묵적으로 정의된 constructor의 super 호출을 통해 수퍼클래스의 constructor에 전달된다.
// 수퍼클래스
class Base {
constructor(a, b) {
this.a = a;
this.b = b;
}
}
// 서브클래스
class Derived extends Base {
// 다음과 같이 암묵적으로 constructor가 정의된다.
// constructor(...args) { super(...args); }
}
const derived = new Derived(1, 2);
console.log(derived); // Derived {a: 1, b: 2}
super를 호출할 때 주의할 사항
- 서브클래스에서 constructor를 생략하지 않을 경우 반드시 super를 호출해야한다.
(서브클래스가 자신이 직접 인스턴스를 생성하지 않고 수퍼클래스에게 인스턴스 생성을 위임하기 때문에)- 서브클래스의 constructor에서 super를 호출하기 전에는 this를 참조할 수 없다.
- super는 반드시 서브 클래스의 constructor에서만 호출한다.
메서드 내에서 super를 참조하면 수퍼클래스의 메서드를 호출할 수 있다.
// 수퍼클래스
class Base {
constructor(name) {
this.name = name;
}
sayHi() {
return `Hi! ${this.name}`;
}
}
// 서브클래스
class Derived extends Base {
sayHi() {
// super.sayHi는 수퍼클래스의 프로토타입 메서드
return `${super.sayHi()}. how are you doing?`;
}
}
const derived = new Derived("Lee");
console.log(derived.sayHi()); // Hi! Lee. how are you doing?
마지막으로 클래스의 생성과정을 정리해보자면,
서브클래스의 super호출
서브클래스는 자신이 직접 인스턴스를 생성하지 않고 수퍼클래스에게 인스턴스 생성을 위임한다. 이것이 바로 서브클래스의 constructor에서 반드시 super를 호출해야 하는 이유다.
수퍼클래스의 인스턴스 생성과 this 바인딩
이때 인스턴스는 수퍼클래스가 생성한 것이지만 new 연산자와 함께 호출된 클래스가 서브클래스이기 때문에 인스턴스는 new.target이 가리키는 서브클래스가 생성한 것으로 처리된다.
수퍼클래스의 인스턴스 초기화
서브클래스 constructor로의 복귀와 this 바인딩
super가 호출되지 않으면 인스턴스가 생성되지 않으며, this 바인딩도 할 수 없다. constructor에서 super를 호출하기 전에 this를 참조할 수 없는 이유가 이 때문이다.
서브클래스 인스턴스 초기화
인스턴스 반환
클래스의 모든 처리가 끝나면 super가 반환한 인스턴스가 this에 바인딩 된다.