ES6에 도입된 클래스 문법은 객체 지향 프로그래밍에서 사용되는 중요한 개념이며, 객체의 설계도 또는 템플릿으로 생각할 수 있다.
클래스를 사용해 객체를 생성하면 코드를 논리적으로 구성하고 유지보수하기 쉽게 만들 수 있다.
사실상 기존의 프로토타입 기반 시스템 위에 구조적인 문법을 제공하는 것이며, 내부적으로는 프로토타입 방식으로 작동된다.
즉, 클래스 문법을 사용하여 객체를 생성하고 상속하는 것은 내부적으로 여전히 프로토타입 체인과 관련이 있다.
ES6 클래스는 class 키워드를 사용하여 정의한다.
클래스 필드의 선언과 초기화는 반드시 constructor 내부에서 실행하고, constructor 내부에 선언한 클래스 필드는 클래스가 생성할 인스턴스에 바인딩 된다.
일반적으로 클래스 이름은 생성자 함수와 마찬가지로 파스칼 케이스 즉, 첫 글자를 대문자로 사용한다.
class Me {
constructor(name, age) {
this.name = name;
this.age = age;
}
sayHoo() {
console.log(`sayHoo~ ${this.name}`);
}
}
const me = new Me('덕배', 30);
me.sayHoo(); // 출력: sayHoo~ 덕배
console.log(me.age); // 30
console.log(me instanceof Me); // true
클래스는 클래스 선언문 이전에 참조할 수 없다. 그렇다고 호이스팅이 발생하지 않는 것은 아니다.
클래스는 var 키워드로 선언한 변수처럼 호이스팅되지 않고, let, const 키워드로 선언한 변수처럼 호이스팅된다.
const Me = '';
{
// 호이스팅이 발생하지 않는다면 ''가 출력
console.log(Me);
// ReferenceError: Cannot access 'Me' before initialization
class Me {}
}
클래스를 사용해 인스턴스를 생성하려면 new 키워드를 사용해야 한다.
new 키워드를 사용하지 않고 constructor를 호출하면 타입 에러가 발생한다.
// new 키워드 O
class Me {}
const me = new Me('덕배', 30);
const mee = new Me('덕수', 35);
// new 키워드 X
class Me {}
const me = Me('덕배', 30);
const mee = Me('덕수', 35);
// TypeError: Class constructor Me cannot be invoked without 'new'
constructor는 인스턴스를 생성하고 클래스 필드를 초기화하기 위한 특수한 메소드다.
-> 클래스 필드는 캡슐화된 변수를 말하며, 데이터 멤버 또는 멤버 변수라고도 한다. 쉽게 말해, 자바스크립트의 생성자 함수에서 this에 추가한 프로퍼티를 클래스 기반 클래스 필드라고 부른다.
constructor는 클래스 내에 한 개만 존재할 수 있고, 2개 이상이면 문법 에러가 발생한다.
인스턴스를 생성할 때 new 키워드와 함께 호출한 것이 바로 constructor이며 constructor의 파라미터에 전달한 값은 클래스 필드에 할당된다.
class Me {
constructor(name, age) {
// this는 클래스가 생성할 인스턴스를 가리킴
// name, age는 클래스 필드
this.name = name;
this.age = age;
}
sayHoo() {
console.log(`sayHoo~ ${this.name} 나이가 벌써 ${this.age}?`);
}
}
// 인스턴스 생성
const me = new Me('덕배', 30);
me.sayHoo();
// 출력: sayHoo~ 덕배 나이가 벌써 30?
constructor는 생략 가능하고, 생략하면 클래스에 constructor를 포함한 것과 동일하게 동작하며 빈 객체를 생성한다.
그렇기에 인스턴스에 프로퍼티를 추가하려면 인스턴스를 생성한 후, 프로퍼티를 동적으로 추가해야 한다.
단, constructor는 인스턴스의 생성과 동시에 클래스 필드의 생성과 초기화를 실행하니까, 클래스 필드 초기화가 필요하다면 constructor를 생략해서는 안된다.
class Me {}
const me = new Me();
console.log(me); // 출력: Me {}
me.num = 5;
console.log(me); // 출력: Me { num: 5 }
class Calculator {
plus(x, y) {
return x + y;
}
minus(x, y) {
return x - y;
}
}
const calc = new Calculator();
console.log(calc.minus(80, 50)); // 출력: 30
console.log(calc.plus(20, 50)); // 출력: 70
// [] 대괄호로 둘러싸서 메소드의 이름으로 사용할 수도 있음
const methodName = 'sayHoo'; // 클래스 메소드 이름
class Me {
constructor({ name, age }) {
this.name = name;
this.age = age;
}
// 아래 메소드 이름은 `sayHoo`
[methodName]() {
return `sayHoo~ ${this.name} 나이가 벌써 ${this.age}?`;
}
}
console.log(new Me({ name: '덕배쓰', age: 30 }).sayHoo());
// 출력: sayHoo~ 덕배쓰 나이가 벌써 30?
getter는 클래스 필드에 값을 반환하는데 사용되며, 주로 클래스의 내부 상태를 외부에 노출하고, 값을 읽는 동작에 관여할 때 유용하게 사용된다.
getter는 메소드 이름 앞에 get 키워드를 사용해 정의한다. 이때 메소드 이름은 클래스 필드 이름처럼 사용된다.
getter는 호출하는 것이 아니라 프로퍼티처럼 참조하는 형식으로 사용하며 참조 시에 메소드가 호출된다. ( obj.property )
getter는 이름 그대로 무언가를 취득할 때 사용하므로 반드시 무언가를 반환해야 한다. 반환문이 없다면 undefined가 반환된다.
class Me {
constructor(arr = []) {
this._arr = arr;
}
get info() {
// getter는 반드시 무언가를 반환함
return this._arr.length ? this._arr[0] : null;
}
}
const me = new Me([1, 2]);
// 필드 info에 접근하려면 getter가 호출됨
console.log(me.info); // 출력: 1
setter는 클래스 필드에 값을 할당하는데 사용되며, 주로 클래스의 내부 상태를 외부에서 변경할 때 유용하게 사용된다.
setter는 메소드 이름 앞에 set 키워드를 사용해 정의한다. 이때 메소드 이름은 클래스 필드 이름처럼 사용된다.
setter는 호출하는 것이 아니라 프로퍼티처럼 할당하는 형식으로 사용하며 할당 시에 메소드가 호출된다. ( obj.property = value )
setter는 인자를 받아서 클래스 필드에 할당하는데, 이때 받은 값을 가공하거나 검증하는 등의 추가적인 로직을 수행할 수 있다.
class Me {
constructor(arr = []) {
this._arr = arr;
}
get info() {
// getter는 반드시 무언가를 반환함
return this._arr.length ? this._arr[0] : null;
}
set info(elem) {
// this._arr 개별 요소로 분리
this._arr = [elem, ...this._arr];
}
}
const me = new Me([1, 2]);
// 클래스 필드 info에 접근하려면 getter가 호출됨
console.log(me.info); // 출력: 1
// 클래스 필드 info에 값을 할당하면 setter가 호출됨
me.info = 100;
console.log(me.info); // 출력: 100
// 이후 배열
console.log(me._arr); // 출력: [ 100, 1, 2 ]
클래스의 정적 메소드를 정의할 때 static 키워드를 사용한다. 정적 메소드는 클래스의 인스턴스가 아닌 클래스 이름으로 호출한다. 따라서 클래스의 인스턴스를 생성하지 않아도 호출할 수 있다.
클래스의 정적 메소드는 인스턴스로 호출할 수 없으며, 이것은 정적 메소드는 this를 사용할 수 없다는 것을 의미한다.
class Me {
constructor(prop) {
this.prop = prop;
}
// 정적 메소드
static staticMethod() {
return 'staticMethod';
}
prototypeMethod() {
return this.prop;
}
}
// 정적 메소드는 클래스 이름으로 호출
console.log(Me.staticMethod());
const me = new Me(123);
// 정적 메소드는 인스턴스를 호출할 수 없음
console.log(me.staticMethod()); // TypeError: me.staticMethod is not a function
클래스 상속을 통해 한 클래스의 기능을 다른 클래스에서 재사용할 수 있다.
새롭게 정의할 클래스가 기존에 있는 클래스와 매우 유사하다면, 상속을 통해 그대로 사용하되 다른 점만 구현하면 된다.
extends 키워드는 부모 클래스를 상속받는 자식 클래스를 정의할 때 사용한다.
이 관계를 부모 클래스 - 자식 클래스 혹은 슈퍼 클래스 ( superclass ) - 서브 클래스 ( subclass ) 관계라고도 말한다.
class Me {
constructor(name) {
this.name = name;
}
sayHoo() {
console.log(`${this.name}는 나야나!`);
}
}
// 자식 클래스
class Mee extends Me {
constructor(name, age) {
super(name);
this.age = age;
}
sayYoo() {
console.log(`${this.name}는 또다른 나야나!`);
}
}
// 인스턴스 생성
const me = new Mee('김덕배', '김덕수');
// 상속된 메소드 호출
me.sayHoo(); // 출력: 김덕배는 나야나!
// 자식 클래스의 메소드 호출
me.sayYoo(); // 출력: 김덕배는 또다른 나야나!
// 부모 클래스
class Me {
sayHoo() {
console.log('나는 덕배');
}
}
// 자식 클래스
class Mee {
// 부모 클래스의 메소드를 오버라이딩
sayHoo() {
console.log('나는 덕수');
}
}
// 인스턴스 생성
const me = new Mee();
// 오버라이딩된 메소드 호출
me.sayHoo(); // 출력: 나는 덕수
오버로딩 ( Overloading ) 은 매개변수의 타입 또는 개수가 다른, 같은 이름의 메소드를 구현하고 매개변수에 의해 메소드를 구별하여 호출하는 방식이다.
자바스크립트는 오버로딩을 지원하지 않지만 arguments 객체를 사용하여 구현할 수는 있다.
// 함수 오버로딩을 시뮬레이션하기 위해 arguments 객체 사용
function add() {
if (arguments.length === 2) {
return arguments[0] + arguments[1];
} else if (arguments.length === 3) {
return arguments[0] + arguments[1] + arguments[2];
} else {
console.error('인수 개수 잘못됨');
}
}
// 오버로딩된 함수 호출
console.log('1:', add(2, 3)); // 출력: 5
console.log('2:', add(2, 3, 4)); // 출력: 9
console.log('3:', add(2, 3, 4, 5)); // 출력: 인수 개수 잘못됨 undefined
super 키워드는 부모 클래스의 필드 또는 메소드를 참조할 때 또는 부모 클래스의 생성자를 호출할 때 사용한다. 즉, 부모 클래스의 인스턴스를 생성한다.
자식 클래스의 constructor에서 super( )를 호출하지 않으면 this에 대한 참조 에러(ReferenceError)가 발생한다. 이것은 super 메소드를 호출하기 이전에는 this를 참조할 수 없음을 의미한다.
class Me {
constructor(name) {
this.name = name;
}
sayHoo() {
console.log(`${this.name}는 나야나!`);
}
}
// 자식 클래스
class Mee extends Me {
constructor(name, age) {
super(name); // super 키워드를 사용해 부모 클래스의 생성자 호출
this.age = age;
}
sayYoo() {
console.log(`${this.name}는 또다른 나야나!`);
}
}
// 인스턴스 생성
const me = new Mee('김덕배', '김덕수');
// 상속된 메소드 호출
me.sayHoo(); // 출력: 김덕배는 나야나!
// 자식 클래스의 메소드 호출
me.sayYoo(); // 출력: 김덕배는 또다른 나야나!
자식 클래스의 정적 메소드 내부에서도 super 키워드를 사용해 부모 클래스의 정적 메소드를 호출할 수 있다.
왜? 자식 클래스는 프로토타입 체인에 의해 부모 클래스의 정적 메소드를 참조할 수 있기 때문이다.
// static 메소드 상속
class Me {
static staticMethod() {
console.log('나는 부모 클래스');
}
}
class Mee extends Me {
static staticMethod() {
super.staticMethod();
console.log('나는 자식 클래스');
}
}
Mee.staticMethod();
// 출력 :
// 나는 부모 클래스
// 나는 자식 클래스
그러나, 자식 클래스의 프로토타입 매소드 내부에서는 super 키워드를 사용해 부모 클래스의 정적 메소드를 호출할 수 없다.
왜? 자식 클래스의 인스턴스는 프로토타입 체인에 의해 부모 클래스의 정적 메소드를 참조할 수 없기 때문이다.
// static 메소드와 prototype 메소드의 상속
class Me {
static staticMethod() {
return '나는 덕배';
}
}
class Mee extends Me {
static staticMethod() {
return `${super.staticMethod()} 하이!`;
}
prototypeMethod() {
return `${super.staticMethod()} 하이루!`;
}
}
console.log(Me.staticMethod()); // 출력: 나는 덕배
console.log(Mee.staticMethod()); // 출력: 나는 덕배 하이!
console.log(new Mee().prototypeMethod());
// TypeError: (intermediate value).staticMethod is not a function
클래스에서는 접근 제어자를 사용해 클래스의 속성 및 메소드의 접근을 제한할 수 있다.
public, protected, private 3가지 접근 제어자를 제공하며, 기본적으로 모든 멤버는 public으로 선언된다.
✓ public: 클래스 내부 및 외부에서 접근 가능
✓ protected: 클래스 내부 및 하위 클래스에서만 접근 가능
✓ private: 클래스 내부에서만 접근 가능
JavaScript에서는 TypeScript와 같은 정적 타입 언어와 달리, 명시적으로 public, protected, private 등의 접근 제어자를 사용하지는 않는다.
따라서 아래 코드의 protected와 private 키워드는 주석으로 표시한 것처럼 실제로 동작하지 않는다.
class Me {
name; // public
protected age;
private email;
constructor(name, age, email) {
this.name = name;
this.age = age;
this.email = email;
}
getAge() {
return this.age;
// protected 멤버는 클래스 내부에서 접근 가능
}
}
const me = new Me('덕배', 30, 'duckbae@gmail.com')
console.log(me.name); // 덕배
console.log(me.age); // Error: age is protected
console.log(me.email); // Error: email is private
console.log(me.getAge()); // 30