클래스

ES5에서는 생성자 함수와 프로토타입을 사용하여 객체 지향 프로그래밍을 구현하였다.

// ES5  
var  Person  =  (function  ()  {  
	// Constructor  
	function  Person(name)  {  
		this._name  =  name;  
	}  
	// public method  
	Person.prototype.sayHi  =  function  ()  {
		console.log('Hi! '  +  this._name);  
	};  
	// return constructor  
	return  Person;  
}());  
var  me  =  new  Person('Lee');  
me.sayHi();  // Hi! Lee.  
console.log(me  instanceof  Person);  // true

클래스 정의(Class Definition)

class  Person  {  
	_name = '';
	constructor(name)  {  
		this._name  =  name;  
	}  
	sayHi()  {  
		console.log(`Hi! ${this._name}`);  
	}  
	/* sayHi = function(){
		console.log(`Hi! ${this._name}`);
	} */
}  
const  me  =  new  Person('Lee');  
me.sayHi();  // Hi! Lee  
console.log(me  instanceof  Person);  // true

인스턴스의 생성

표현식이 아닌 선언식으로 정의한 클래스의 이름은 constructor와 동일하다.

class  Foo  {}  
const  foo  =  new  Foo();
console.log(Foo  ===  Foo.prototype.constructor);  // true

constructor

constructor는 인스턴스를 생성하고 클래스 프로퍼티를 초기화하기 위한 특수한 메소드이다. constructor는 클래스 내에 한 개만 존재할 수 있으며 만약 클래스가 2개 이상의 constructor를 포함하면 문법 에러(SyntaxError)가 발생한다. 인스턴스를 생성할 때 new 연산자와 함께 호출한 것이 바로 constructor이며 constructor의 파라미터에 전달한 값은 클래스 프로퍼티에 할당한다.

constructor는 생략할 수 있다. constructor를 생략하면 클래스에 constructor() {}를 포함한 것과 동일하게 동작한다. 즉, 빈 객체를 생성한다. 따라서 클래스 프로퍼티를 선언하려면 인스턴스를 생성한 이후, 클래스 프로퍼티를 동적 할당해야 한다.

class  Foo  {  }  
const  foo  =  new  Foo();  
console.log(foo);  // Foo {}  
// 클래스 프로퍼티의 동적 할당 및 초기화  
foo.num  =  1;  
console.log(foo);  // Foo { num: 1 }

constructor는 인스턴스의 생성과 동시에 클래스 프로퍼티의 생성과 초기화를 실행한다.

class  Foo  {  
	// constructor는 인스턴스의 생성과 동시에 클래스 프로퍼티의 생성과 초기화를 실행한다.  
	constructor(num)  {  
		this.num  =  num;  
	}  
}  
const  foo  =  new  Foo(1);  
console.log(foo);  // Foo { num: 1 }

클래스 프로퍼티

클래스 몸체(class body)에는 메소드만 선언할 수 있다. 클래스 바디에 클래스 프로퍼티(인스턴스 필드, 멤버 변수)를 선언하면 문법 에러(SyntaxError)가 발생한다. 클래스 프로퍼티의 선언과 초기화는 반드시 constructor 내부에서 실시한다.

class  Foo  {  
	constructor(name  =  '')  {  
		this.name  =  name;  // 클래스 프로퍼티의 선언과 초기화  
	}  
	name  =  '';  // SyntaxError
}  
const  foo  =  new  Foo('Lee');  
console.log(foo);  // Foo { name: 'Lee' }

constructor 내부에서 선언한 클래스 프로퍼티는 클래스의 인스턴스를 가리키는 this에 바인딩한다. 이로써 클래스 프로퍼티는 클래스가 생성할 인스턴스의 프로퍼티가 되며, 클래스의 인스턴스를 통해 클래스 외부에서 언제나 참조할 수 있다. 즉, 언제나 public이다. ES6의 클래스는 다른 객체지향 언어처럼 private, public, protected 키워드와 같은 접근 제한자(access modifier)를 지원하지 않는다.

호이스팅

class 선언문 이전에 클래스를 참조하면 참조 에러(ReferenceError)가 발생한다.

getter, setter

getter

getter는 클래스 프로퍼티에 접근할 때마다 클래스 프로퍼티의 값을 조작하는 행위가 필요할 때 사용한다. getter는 메소드 이름 앞에 get 키워드를 사용해 정의한다. 이때 메소드 이름은 클래스 프로퍼티 이름처럼 사용된다. 다시 말해 getter는 호출하는 것이 아니라 프로퍼티처럼 참조하는 형식으로 사용하며 참조 시에 메소드가 호출된다. getter는 이름 그대로 무언가를 취득할 때 사용하므로 반드시 무언가를 반환해야 한다. 사용 방법은 아래와 같다.

class  Foo  {  
	constructor(arr  =  [])  {  
		this._arr  =  arr;  
	}  
	// getter: get 키워드 뒤에 오는 메소드 이름 firstElem은 프로퍼티 이름처럼 사용된다.  
	get  firstElem()  {  
		// getter는 반드시 무언가를 반환해야 한다.  
		return  this._arr.length  ?  this._arr[0]  :  null;  
	}  
}  
const  foo  =  new  Foo([1,  2]);  
// 프로퍼티 firstElem에 접근하면 getter가 호출된다.  
console.log(foo.firstElem);  // 1

setter

setter는 클래스 프로퍼티에 값을 할당할 때마다 클래스 프로퍼티의 값을 조작하는 행위가 필요할 때 사용한다. setter는 메소드 이름 앞에 set 키워드를 사용해 정의한다. 이때 메소드 이름은 클래스 프로퍼티 이름처럼 사용된다. 다시 말해 setter는 호출하는 것이 아니라 프로퍼티처럼 값을 할당하는 형식으로 사용하며 할당 시에 메소드가 호출된다. 사용 방법은 아래와 같다.

class  Foo  {  
	constructor(arr  =  [])  {  
		this._arr  =  arr;  
	}  
	// getter: get 키워드 뒤에 오는 메소드 이름 firstElem은 프로퍼티 이름처럼 사용된다.  
	get  firstElem()  {  
	// getter는 반드시 무언가를 반환하여야 한다.  
		return  this._arr.length  ?  this._arr[0]  :  null;  
	}  
	// setter: set 키워드 뒤에 오는 메소드 이름 firstElem은 프로퍼티 이름처럼 사용된다.  
	set  firstElem(elem)  {  
		// ...this._arr은 this._arr를 개별 요소로 분리한다  
		this._arr  =  [elem,  ...this._arr];  
	}  
}  
const  foo  =  new  Foo([1,  2]);  
// 프로퍼티 lastElem에 값을 할당하면 setter가 호출된다.  
foo.firstElem  =  100;  
console.log(foo.firstElem);  // 100

정적 메소드

클래스의 정적(static) 메소드를 정의할 때 static 키워드를 사용한다. 정적 메소드는 클래스의 인스턴스가 아닌 클래스 이름으로 호출한다. 따라서 클래스의 인스턴스를 생성하지 않아도 호출할 수 있다.

class  Foo  {  
	constructor(prop)  {  
		this.prop  =  prop;  
	}  
	static  staticMethod()  {  
		/*  정적 메소드는 this를 사용할 수 없다.  정적 메소드 내부에서 this는 클래스의 인스턴스가 아닌 클래스 자신을 가리킨다.  */  
		return  'staticMethod';  
	}  
	prototypeMethod()  {  
		return  this.prop;  
	}  
}  
// 정적 메소드는 클래스 이름으로 호출한다.  
console.log(Foo.staticMethod());  
const  foo  =  new  Foo(123);  
// 정적 메소드는 인스턴스로 호출할 수 없다.  
console.log(foo.staticMethod());  // Uncaught TypeError: foo.staticMethod is not a function

정적 메소드는 Math 객체의 메소드처럼 애플리케이션 전역에서 사용할 유틸리티(utility) 함수를 생성할 때 주로 사용한다.

클래스 상속

extends 키워드

extends 키워드는 부모 클래스(base class)를 상속받는 자식 클래스(sub class)를 정의할 때 사용한다. 부모 클래스 Circle을 상속받는 자식 클래스 Cylinder를 정의해 보자.

// 부모 클래스  
class  Circle  {  
	constructor(radius)  {  
		this.radius  =  radius;  // 반지름  
	}  
	// 원의 지름  
	getDiameter()  {  
		return  2  *  this.radius;  
	}  
	// 원의 둘레  
	getPerimeter()  {  
		return  2  *  Math.PI  *  this.radius;  
	}  
	// 원의 넓이  
	getArea()  {  
		return  Math.PI  *  Math.pow(this.radius,  2);  
	}  
}  
// 자식 클래스  
class  Cylinder  extends  Circle  {  
	constructor(radius,  height)  {  
		super(radius);  
		this.height  =  height;  
	}  
	// 원통의 넓이: 부모 클래스의 getArea 메소드를 오버라이딩하였다.  
	getArea()  {  
		// (원통의 높이 * 원의 둘레) + (2 * 원의 넓이)
	  return  (this.height  *  super.getPerimeter())  +  (2  *  super.getArea());  
  }  
  // 원통의 부피  
  getVolume()  {  
	  return  super.getArea()  *  this.height;  
  }  
}  
// 반지름이 2, 높이가 10인 원통  
const  cylinder  =  new  Cylinder(2,  10);  
// 원의 지름  
console.log(cylinder.getDiameter());  // 4  
// 원의 둘레  
console.log(cylinder.getPerimeter());  // 12.566370614359172  
// 원통의 넓이  
console.log(cylinder.getArea());  // 150.79644737231007  
// 원통의 부피  
console.log(cylinder.getVolume());  // 125.66370614359172  
// cylinder는 Cylinder 클래스의 인스턴스이다.  
console.log(cylinder  instanceof  Cylinder);  // true  
// cylinder는 Circle 클래스의 인스턴스이다.  
console.log(cylinder  instanceof  Circle);  // true

super 키워드

super 키워드는 부모 클래스를 참조(reference) 할 때 또는 부모 클래스의 constructor를 호출할 때 사용한다. super가 메소드로 사용될 때, 그리고 객체로 사용될 때 다르게 동작한다.

// 부모 클래스
class Circle {
...
}
class Cylinder extends Circle {
  constructor(radius, height) {
    // ① super 메소드는 부모 클래스의 인스턴스를 생성
    super(radius);
    this.height = height;
  }
  // 원통의 넓이: 부모 클래스의 getArea 메소드를 오버라이딩하였다.
  getArea() {
    // (원통의 높이 * 원의 둘레) + (2 * 원의 넓이)
    // ② super 키워드는 부모 클래스(Base Class)에 대한 참조
    return (this.height * super.getPerimeter()) + (2 * super.getArea());
  }
  // 원통의 부피
  getVolume() {
    // ② super 키워드는 부모 클래스(Base Class)에 대한 참조
    return super.getArea() * this.height;
  }
}
// 반지름이 2, 높이가 10인 원통
const cylinder = new Cylinder(2, 10);

① super 메소드는 자식 class의 constructor 내부에서 부모 클래스의 constructor(super-constructor)를 호출한다. 즉, 부모 클래스의 인스턴스를 생성한다. 자식 클래스의 constructor에서 super()를 호출하지 않으면 this에 대한 참조 에러(ReferenceError)가 발생한다. ② super 키워드는 부모 클래스(Base Class)에 대한 참조이다. 부모 클래스의 프로퍼티 또는 메소드를 참조하기 위해 사용한다.

static 메소드와 prototype 메소드의 상속

프로토타입 관점에서 바라보면 자식 클래스의 [[Prototype]] 프로퍼티가 가리키는 프로토타입 객체는 부모 클래스이다.

class Parent {}
class Child extends Parent {}
console.log(Child.__proto__ === Parent); // true
console.log(Child.prototype.__proto__ === Parent.prototype); // true

Prototype chain에 의해 부모 클래스의 정적 메소드도 상속된다.

class Parent {
  static staticMethod() {
    return 'staticMethod';
  }
}
class Child extends Parent {}
console.log(Parent.staticMethod()); // 'staticMethod'
console.log(Child.staticMethod());  // 'staticMethod'

자식 클래스의 정적 메소드 내부에서도 super 키워드를 사용하여 부모 클래스의 정적 메소드를 호출할 수 있다. 이는 자식 클래스는 프로토타입 체인에 의해 부모 클래스의 정적 메소드를 참조할 수 있기 때문이다.

하지만 자식 클래스의 일반 메소드(프로토타입 메소드) 내부에서는 super 키워드를 사용하여 부모 클래스의 정적 메소드를 호출할 수 없다. 이는 자식 클래스의 인스턴스는 프로토타입 체인에 의해 부모 클래스의 정적 메소드를 참조할 수 없기 때문이다.

class Parent {
  static staticMethod() {
    return 'Hello';
  }
}
class Child extends Parent {
  static staticMethod() {
    return `${super.staticMethod()} wolrd`;
  }
  prototypeMethod() {
    return `${super.staticMethod()} wolrd`;
  }
}
console.log(Parent.staticMethod()); // 'Hello'
console.log(Child.staticMethod());  // 'Hello wolrd'
console.log(new Child().prototypeMethod());
// TypeError: (intermediate value).staticMethod is not a function