객체지향 프로그래밍에서 정해진 변수와 메소드를 공유하는 객체를 생성하기 위해 사용하는 일종의 툴이다. 객체를 정의하기 위한 상태와 메서드로 구성되어 있다.
class {
constructor() {...}
method() {...}
}
constructor()
: 객체의 기본 상태를 설정해주는 생성자 메서드이며, new
키워드에 의해 자동으로 호출되고 함수를 생성한다. cunstroctor
메서드 내부의 코드가 함수의 본문이 된다.method()
: 생성된 함수의 Prototype
에 저장된다.생성자 함수 동작방식과 굉장히 유사하다. 그렇다면 왜 class
문법을 사용할까?
class
로 만든 함수엔 특수 내부 프로퍼티인 [[FunctionKind]]:"classConstructor
가 존재한다. 자바스크립트는 함수 실행시 다양한 방법을 통해 이 프로퍼티를 확인한다.
클래스 메서드 프로퍼티 플래그의 enumerable
속성 값은 false
이다. for..in
문으로 객체를 순회할 때, 메서드는 순회 대상에서 제거하고 싶은 경우가 자주 있으므로 꽤나 유용하다.
클래스는 항상 strict mode
로 실행된다.
함수처럼 클래스도 다른 표현식 내부에서 정의, 전달, 반환, 할당할 수 있다.
클래스는 표현식으로 정의가 가능하며, 표현식에서 클래스에 이름을 부여할 수 있으며 이 경우엔 내부에서만 클래스명을 사용할 수 있다. 동적으로 생성 또한 가능하다.
class {
name = "SMP";
method() {
}
}
위와 같이 name = "SMP"
처럼, propertyName = value
형식의 코드를 통해 Prototype
에 저장되는 것이 아닌, 개별 객체에만 적용되는 클래스 필드를 설정한다. 클래스필드는 생성자의 역할이 끝난후에 역할을 하기 때문에, 형태에 제약이 없는 프로퍼티를 클래스에 추가할 수 있다. 이는 this
바인딩 문제를 해결하는데 쓰이기도 한다.
class Button {
constructor(value) {
this.value = value;
}
click() {
alert(this.value);
}
}
let button = new Button("hello");
setTimeout(button.click, 1000); // undefined
함수 바인딩은 동적이기 때문에 위와 같은 경우, click()
호출되는 시점의 실행컨텍스트의 this
는 button 객체에 바인딩되지 않고 window
에 바인딩되기 때문에 undefined가 출력된다.
class Button {
constructor(value) {
this.value = value;
}
click = () => {
alert(this.value);
}
}
let button = new Button("hello");
setTimeout(button.click, 1000); // hello
여기서 클래스필드를 이용하여 화살표함수 프로퍼티를 추가하면 , 화살표 함수는 선언된 시점의 외부 scope를 계승받기 때문에 여기서 this
는 class Button
의 this
를 가리키게 되어 알맞게 출력된다.
클래스 상속을 이용하면, 클래스를 다른 클래스로 확장시킬 수 있다. 기존에 존재하던 기능을 토대로 새로운 기능도 만들 수 있다.
extends
키워드는 프로토타입을 기반으로 동작한다. extends
는 Child.prototype.[[Prototype]]
를 Parent.portotype
으로 설정한다. 프로토타입 동작방식 그대로 동작한다.
function f(phrase) {
return class {
sayHi() { alert(phrase) }
}
}
class User extends f("Hello") {}
new User().sayHi(); // Hello
extends
뒤에 표현식이 올 수도 있다.
class Child
는 class Parent
의 메서드를 그대로 상속 받는다. 하지만, class Child
에서 class Parent
에 있는 메서드를 재정의하게 되면 재정의한 자체 메서드가 사용된다. 부모 메서드 전체를 대체하지 않고 부모 메서드를 토대로 일부 기능만 변경하고 싶은 경우, 커스텀 메서드를 사용하여 작업하게 되는데, 이미 커스텀 메서드를 만들었더라도 부모 메서드를 호출하고 싶은 상황이 있는데 이 때 super
키워드를 사용한다.
super.method(...)
는 부모 클래스에 정의된 메서드, method
를 호출한다.super(...)
는 부모 생성자를 호출하는데, 자식 생성자 내부에서만 사용할 수 있다.클래스 상속시, constructor
를 따로 명시하지 않는 경우, 아래와 같이 빈 constructor
가 만들어진다.
class Child extends Parent {
// 자체 생성자가 없는 클래스를 상속받으면 자동으로 만들어짐
constructor(...args) {
super(...args);
}
}
생성자는 기본적으로 부모 constructor
를 호출한다. 이때 부모 constructor
에도 인수를 모두 전달한다. 위에서 언급했듯이, 따로 생성자가 없는경우 이 과정이 자동으로 일어난다.
상속 클래스의 생성자에서는 반드시 super(...)
를 호출해야하며, this
를 사용하기 전에 반드시 호출해야 한다. 그 이유는 다음과 같다.
자바스크립트는 상속 클래스의 생성자 함수(derived constructor)와 그렇지 않은 생성자 함수를 특수 내부 프로퍼티
[[ConstructorKind]]:"derived"`로 판별한다.
일반 클래스가 new
와 함께 실행되면, 빈 객체가 만들어지고 this
에 이 객체를 할당한다.
상속 클래스의 생성자 함수가 실행되면, 일반 클래스에서의 과정이 일어나지 않고, 이 과정을 부모 클래스의 생성자가 처리해주길 기다린다.
따라서, 상속 클래스의 생성자에선 super
를 호출해 부모 생성자를 실행해 주어야 한다. 그렇지 않으면 this
바인딩이 안되어 에러가 발생한다.
클래스 필드 오버라이딩은 메서드 오버라이딩과 달리, 부모 클래스 필드가 부모 constructor
에서 사용될 경우 주의해야 한다. 부모 constructor
는 언제나 자기 자신의 필드 값을 사용한다. 이는 클래스 필드 초기화의 시점 때문인데, 부모 클래스에서는 constructor
이전에, 상속 클래스에서는 constructor
직후에 초기화 되기 때문이다. 따라서, 상속 클래스에서는 constructor
에 super
키워드로 부모 constructor
가 가장먼저 호출되기 때문에 해당 시점에는 부모 클래스의 클래스 필드만이 존재한다.
클래스의 프로토타입 말고, 클래스 자체에 메서드를 설정할 수 있다. 이를 정적메서드 라고 한다. static
키워드와 함께 쓰인다.
class Print {
static staticMethod() {
alert(this === Print);
}
}
Print.staticMethod(); // true
정적 메서드는 특정 객체가 아니라 클래스의 함수를 구현하고자 할때 주로 사용된다.
class Data {
constructor(name,age) {
this.name = name;
this.age = age;
}
static sorting(dataA,dataB){
return dataA.age - dataB.age
}
}
let data = [
new Data("PSM", 25 ),
new Data("SMP", 22 ),
new Data("MSP", 20 )
];
data.sort(Data.sorting);
console.log( data.name[0] ) // MSP
Data.sorting
은 data를 나이가 적은순으로 비교한다. static
메서드는 이처럼 개별 객체가 아니라, 클래스에 적용되는 함수를 정의하는데 사용한다.
static
키워드로 정적 프로퍼티를 생성할 수 있다.
class Blog {
static owner = "PSM";
}
alert(Blog.owner); // PSM
Blog.owner = "PSM"
과 동일하다.정적프로퍼티와 메서드는 상속되며, extends
키워드를 통해 프로토타입체이닝으로 상속이 이루어진다. extends
키워드는 2가지의 [[Prototype]]
참조를 만들어내는데, class Child
는 프로토타입을 통해 class Parent
를 상속받고(정적 메서드), Child.prototype
은 Parent.prototype
을 상속받는다(일반 메서드). 따라서, 정적 프로퍼티와 메서드 모두 상속된다.]
객체 지향 프로그래밍에서 프로퍼티와 메서드는 두 그룹으로 분류된다.
내부 인터페이스 : 동일한 클래스 내의 다른 메서드에선 접근할 수 있지만, 클래스 밖에선 접근할 수 없는 프로퍼티
외부 인터페이스 : 클래스 밖에서도 접근 가능한 프로퍼티와 메서드
이는 곧, 클래스의 두가지 타입의 객체 필드(프로퍼티와 메서드)와 연관된다.
private
: 클래스 내부에서만 접근할 수 있으며 내부 인터페이스를 구성할 때 쓰인다. (아직 명세서에 등재되기 전이므로 따로 정리하지 않을 예정)
public
: 어디서든지 접근할 수 있으며 외부 인터페이스를 구성한다.
자바스크립트에서는 protected
필드를 지원하지않지만 , 이를 모방하여 사용한다.
class MobilePhone {
memory = 0;
constructor(power) {
this.power = power;
console.log( `배터리가 ${power} 남았습니다.`);
}
}
// 핸드폰 생성
let myPhone = new MobilePhone(100);
// 메모리 추가
myPhone.memory = 200;
현재 memory
와 power
은 public
이다. 누구나 접근하여 바꿀 수 있다. 이제, memory
를 protected
로 바꾸고, 0 미만의 값으로는 설정될 수 없게 만들어 보자.
class MobilePhone {
_memory = 0;
setMemory(value){
if (value<0) throw new Error(`메모리는 음수가 안됩니다.`)
this._memory = value
}
getMemory() {
return this._memory;
}
constructor(power) {
this._power = power;
}
}
// 핸드폰 생성
let myPhone = new MobilePhone(100);
// 메모리 추가
myPhone.setMemory(-10); // Error
메모리를 음수로 설정하면 실패한다.
프로퍼티 생성시에만 값을 할당하고 뒤로 절대 수정하지 말아야 할 프로퍼티 값들에 대해서 적용한다.
class MobilePhone {
_memory = 0;
setMemory(value){
if (value<0) throw new Error(`메모리는 음수가 안됩니다.`)
this._memory = value
}
getMemory() {
return this._memory;
}
constructor(power) {
this._power = power;
}
get power() {
return this._power
}
}
// 핸드폰 생성
let myPhone = new MobilePhone(100);
// 메모리 추가
myPhone.setMemory(-10); // Error
myPhone.power = 10;
setter
를 만들지 않으면 된다.
배열이나 , 맵 같은 내장 클래스도 확장이 가능하다.
다음 예제를 보자
class PowerArray extends Array {
isEmpty() {
return this.length ===0;
}
}
let arr = new PowerArray(1, 2, 5, 10, 50);
console.log(arr.isEmpty()); //false
let filteredArr = arr.filter(item => item >=10);
console.log(filteredArr); // [10,50]
console.log(filteredArr.isEmpty()); // false
arr.filter()
가 호출되면, 내부에선 기본 Array
가 아니라
PowerArray
의 constructor
를 기반으로 새로운 배열이 만들어지고, 여기에 필터후 결과가 담긴다. 이렇게 되면 PowerArray
에 구현된 메서드를 사용할 수 있다.
특수 정적 getter인 Symbol.species
를 클래스에 추가할 수 있는데, Symbol.species가 있으면 ,
map,
filter`등의 메서드를 호출할 때 만들어지는 개체의 생성자를 지정할 수 있다.
map
이나 filter
같은 내장 메서드가 일반 배열을 반환하도록 하려면 , 아래 예시처럼 Symbol.species
가 Array
를 반환해주도록 하면 된다.
class PowerArray extends Array {
isEmpty() {
return this.length ===0;
}
static get [Symbol.species]() {
return Array;
}
}
let arr = new PowerArray(1,2,5,10,50);
console.log(arr.isEmpty()); // false
// filter 는 arr.constructor[Symbol.species]를 생성자로 사용
let filteredArr = arr.filter(item => item>=10);
console.log(filteredArr.isEmpty()); //Error
filteredArr
는 이제 Array
를 반환하기 때문에, 확장된 PowerArray
의 내부 메서드를 사용할 수 없다.
내장 객체는 Objects.keys
, Array.isArray
등의 자체 정적 메서드를 갖는다.
네이티브 클래스들은 서로 상속관계를 맺는다. Array
는 Object
를 상속받는다.
일반적으로 한 클래스가 상속을 받으면 정적 메서드와 그렇지 않은 메서드를 모두 상속 받는다. 그런데 내장 클래스는 정적 메서드를 상속받지 못한다.
가령, Array
와 Date
는 모두 Object
를 상속받아 Object.prototype
의 메서드를 사용할 수 있다. 하지만, Array.prototype
은 Object
자체를 참조하지 않기 때문에, Array.keys()
같은 정적 메서드를 인스턴스에서 사용할 수 없다.
이것이 내장 객체간의 상속과 extends
를 사용한 상속의 가장 큰 차이이다.
instanceof
로 클래스 확인하기instanceof
연산자를 통해 특정 클래스에 속해있는지 알 수 있다. 또한, 상속관계도 알 수 있다.
obj instanceof Class
obj
가 Class
에 속하거나 상속받는다면 true
가 반환된다.
또한, 생성자 함수에도 사용할 수 있다.
class Rabbit {}
let rabbit = new Rabbit();
console.log( rabbit instanceof Rabbit ); // true
function Turtle() {}
console.log( new Turtle() instanceof Turtle ); // true
instanceof
연산자는 보통, 프로토타입 체인을 거슬러 올라가며 인스턴스 여부나 상속 여부를 확인한다.
obj instanceof Class
의 알고리즘을 이해한다면
정적 메서드 Symbol.hasInstance
를 사용하여 확인 로직을 설정할 수 있다.
Symbol.hasInstance
가 구현되어 있다면, obj instanceof Class
가 실행될 때, Class[Symbol.hasInstance](obj)
가 호출된다. 호출 결과는 true
,false
이어야 한다.이를 통해 instanceof
를 커스터마이징 해보자.
class Human {
static [Symbol.hasInstance](obj) {
if(obj.canSpeak) return true;
}
}
let obj = { canSpeak : true };
console.log (obj instanceof Human ); // true
하지만, 대부분의 클래스엔 Symbol.hasInstance
가 구현되어있지 않고, 이 경우에는 일반적인 로직이 사용된다. obj instanceof Class
는 Class.prototype
이 obj
프로토타입 체인 상의 프로토타입중 하나와 일치하는지 확인한다.
obj.__proto__ === Class.prototype?
obj.__proto__.__proto__ === Class.prototype?
obj.__proto__.__proto__.__proto__ === Class.prototype?
이중 하나라도 true
를 반환하면 instanceof
는 true
를 반환한다. 그러지 않고 체인의 끝에 도달하면 false
를 반환한다.