클래스 기본

Y·2020년 9월 2일
0

자바스크립트

목록 보기
14/20

클래스


객체지향 프로그래밍에서 정해진 변수와 메소드를 공유하는 객체를 생성하기 위해 사용하는 일종의 툴이다. 객체를 정의하기 위한 상태와 메서드로 구성되어 있다.

class {
  constructor() {...}
  method() {...}
}
  • constructor() : 객체의 기본 상태를 설정해주는 생성자 메서드이며, new키워드에 의해 자동으로 호출되고 함수를 생성한다. cunstroctor 메서드 내부의 코드가 함수의 본문이 된다.
  • method() : 생성된 함수의 Prototype에 저장된다.

생성자 함수 동작방식과 굉장히 유사하다. 그렇다면 왜 class문법을 사용할까?

  1. class로 만든 함수엔 특수 내부 프로퍼티인 [[FunctionKind]]:"classConstructor가 존재한다. 자바스크립트는 함수 실행시 다양한 방법을 통해 이 프로퍼티를 확인한다.

  2. 클래스 메서드 프로퍼티 플래그의 enumerable 속성 값은 false이다. for..in문으로 객체를 순회할 때, 메서드는 순회 대상에서 제거하고 싶은 경우가 자주 있으므로 꽤나 유용하다.

  3. 클래스는 항상 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를 계승받기 때문에 여기서 thisclass Buttonthis를 가리키게 되어 알맞게 출력된다.

상속


클래스 상속을 이용하면, 클래스를 다른 클래스로 확장시킬 수 있다. 기존에 존재하던 기능을 토대로 새로운 기능도 만들 수 있다.

extends 키워드


extends 키워드는 프로토타입을 기반으로 동작한다. extendsChild.prototype.[[Prototype]]Parent.portotype으로 설정한다. 프로토타입 동작방식 그대로 동작한다.

function f(phrase) {
  return class {
    sayHi() { alert(phrase) }
  }
}

class User extends f("Hello") {}

new User().sayHi(); // Hello

extends 뒤에 표현식이 올 수도 있다.

  • 조건에 따라서 다른 클래스를 상속받을 수 있다. 조건에 따라 다른 클래스를 반환하는 함수를 만들고 , 함수 호출 결과를 상속받게 만들면 된다.

메서드 오버라이딩


class Childclass 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 직후에 초기화 되기 때문이다. 따라서, 상속 클래스에서는 constructorsuper 키워드로 부모 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.prototypeParent.prototype을 상속받는다(일반 메서드). 따라서, 정적 프로퍼티와 메서드 모두 상속된다.]

protected 프로퍼티와 메서드


객체 지향 프로그래밍에서 프로퍼티와 메서드는 두 그룹으로 분류된다.

  • 내부 인터페이스 : 동일한 클래스 내의 다른 메서드에선 접근할 수 있지만, 클래스 밖에선 접근할 수 없는 프로퍼티

  • 외부 인터페이스 : 클래스 밖에서도 접근 가능한 프로퍼티와 메서드

이는 곧, 클래스의 두가지 타입의 객체 필드(프로퍼티와 메서드)와 연관된다.

  • private : 클래스 내부에서만 접근할 수 있으며 내부 인터페이스를 구성할 때 쓰인다. (아직 명세서에 등재되기 전이므로 따로 정리하지 않을 예정)

  • public : 어디서든지 접근할 수 있으며 외부 인터페이스를 구성한다.

프로퍼티 보호하기


자바스크립트에서는 protected필드를 지원하지않지만 , 이를 모방하여 사용한다.


class MobilePhone {
	memory = 0;
	constructor(power) {
      this.power = power;
      console.log( `배터리가 ${power} 남았습니다.`);
    }
}

// 핸드폰 생성
let myPhone = new MobilePhone(100);

// 메모리 추가
myPhone.memory = 200;

현재 memorypowerpublic 이다. 누구나 접근하여 바꿀 수 있다. 이제, memoryprotected로 바꾸고, 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가 아니라
PowerArrayconstructor 를 기반으로 새로운 배열이 만들어지고, 여기에 필터후 결과가 담긴다. 이렇게 되면 PowerArray에 구현된 메서드를 사용할 수 있다.

특수 정적 getter인 Symbol.species를 클래스에 추가할 수 있는데, Symbol.species가 있으면 , map,filter`등의 메서드를 호출할 때 만들어지는 개체의 생성자를 지정할 수 있다.

map이나 filter같은 내장 메서드가 일반 배열을 반환하도록 하려면 , 아래 예시처럼 Symbol.speciesArray를 반환해주도록 하면 된다.

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 등의 자체 정적 메서드를 갖는다.

네이티브 클래스들은 서로 상속관계를 맺는다. ArrayObject를 상속받는다.
일반적으로 한 클래스가 상속을 받으면 정적 메서드와 그렇지 않은 메서드를 모두 상속 받는다. 그런데 내장 클래스는 정적 메서드를 상속받지 못한다.

가령, ArrayDate는 모두 Object를 상속받아 Object.prototype의 메서드를 사용할 수 있다. 하지만, Array.prototypeObject자체를 참조하지 않기 때문에, Array.keys()같은 정적 메서드를 인스턴스에서 사용할 수 없다.

이것이 내장 객체간의 상속과 extends를 사용한 상속의 가장 큰 차이이다.

instanceof로 클래스 확인하기


instanceof연산자를 통해 특정 클래스에 속해있는지 알 수 있다. 또한, 상속관계도 알 수 있다.

instanceof 연산자

obj instanceof Class

objClass에 속하거나 상속받는다면 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 ClassClass.prototypeobj프로토타입 체인 상의 프로토타입중 하나와 일치하는지 확인한다.

obj.__proto__ === Class.prototype?
obj.__proto__.__proto__ === Class.prototype?
obj.__proto__.__proto__.__proto__ === Class.prototype?

이중 하나라도 true를 반환하면 instanceoftrue를 반환한다. 그러지 않고 체인의 끝에 도달하면 false를 반환한다.

profile
연세대학교 산업공학과 웹개발 JavaScript

0개의 댓글