[모던자바스크립트] 클래스 정리

클래스는 constructor와 메소드가 들어간다. new 연산자를 사용해서 객체를 생성하면 constructor가 자동으로 호출되고 메소드는 prototype에 들어간다.

정확히 말하면 밑의 예제에서 class 문법이 하는 일은 다음과 같다.

  1. MyClass라는 이름을 가진 함수를 만든다. 함수의 본문은 constructor의 내용으로 채워진다.
  2. 클래스의 메서드를 MyClass.prototype에 저장한다.
class MyClass {
  // 여러 메서드를 정의할 수 있음
  constructor() { ... }
  method1() { ... }
  method2() { ... }
  method3() { ... }
  ...
}

const myClass = new MyClass();

클래스는 함수이다.

alert(typeof User); // function

클래스와 생성자 함수의 차이점

  1. class로 만든 함수에는 특수 내부 프로퍼티 [[FunctionKind]] 라는 것이 있어서 new 연산자와 함께 호출하지 않으면 에러가 발생한다.
  2. 클래스의 메서드는 열거할 수 없다.
  3. 클래스는 항상 엄격모드로 실행된다.

클래스 필드

클래스를 정의할 때 프로퍼티 '<프로퍼티 이름> = <값>'을 써주면 간단히 클래스 필드를 만들 수 있다.

클래스 필드의 중요한 특징은 prototype이 아닌 개별 객체에 클래스 필드가 설정된다는 점이다.

클래스 필드는 생성자가 끝난 뒤에 처리된다.

클래스 필드를 이용하면 this를 객체에 binding 해서 사용할 수 있다.

class Button {
  constructor(value) {
    this.value = value;
  }
  click = () => {
    alert(this.value);
  }
}

let button = new Button("hello");

setTimeout(button.click, 1000); // hello

상속

예를 들어 다음과 같이 상속을 하면,

class Rabbit extends Animal {
  hide() {
    alert(`${this.name} 이/가 숨었습니다!`);
  }
}

let rabbit = new Rabbit("흰 토끼");

rabbit.run(5); // 흰 토끼 은/는 속도 5로 달립니다.
rabbit.hide(); // 흰 토끼 이/가 숨었습니다! 

Rabbit.prototype.[[Prototype]]Animal.prototype으로 설정한다.

오버라이딩

부모 메서드를 기반으로 일부 기능만 변경을 하고 싶을 때 super를 사용한다.

class Rabbit extends Animal {
  hide() {
    alert(`${this.name}가 숨었습니다!`);
  }

  stop() {
    super.stop(); // 부모 클래스의 stop을 호출해 멈추고,
    this.hide(); // 숨습니다.
  }
}

화살표 함수에는 자신의 this나 super가 없기 때문에 외부 함수에서 super를 가져온다.

constructor 오버라이딩

상속 클래스의 생성자에서는 반드시 super(...args); 를 호출해야한다.

자바스크립트에서는 상속 클래스의 생성자 함수와 일반 생성자 함수를 [[ConstructorKind]]:"derived" 프로퍼티를 사용해서 구분한다.

일반 클래스의 생성자 함수와 상속 클래스의 생성자 함수의 차이는

  • 일반 클래스는 new 와 함께 실행되면 빈 객체가 만들어지고 this에 이 객체를 할당한다.
  • 상속 클래스가 new와 함께 실행되면 상속 클래스의 생성자 함수는 빈 객체를 만들고 this에 이 객체를 할당하는 일을 부모 클래스의 생성자 함수가 처리해주길 기다린다.

따라서 상속 클래스의 생성자에서는 super를 호출해 부모 생성자를 실행해주어야한다. 아니면 this가 만들어지지 않아 에러가 발생한다.

엥 그러면 super의 constructor의 내용이 자식이 된다는건가? 상속이 아니네?

class Animal {
  constructor(name="animal") {
    this.speed = 0;
    this.name = name;
  }
	a() {

	}
}

class Rabbit extends Animal {
  constructor(nickname) {
    super();
    this.nickname = nickname;
		this.name = name;
  }
}

let rabbit = new Rabbit("흰 토끼");

const sonRabbit = Object.create(rabbit);
sonRabbit.age = 3;

console.log(rabbit) // Rabbit { speed: 0, name: 'animal', nickname: '흰 토끼' }
console.log(sonRabbit) // Rabbit { age: 3 }
console.log(sonRabbit.nickname) //흰 토끼

필드 오버라이딩

class Animal {
  name = 'animal'

  constructor() {
    alert(this.name); // (*)
  }
}

class Rabbit extends Animal {
  name = 'rabbit';
}

new Animal(); // animal
new Rabbit(); // animal
class Animal {
  showName() {  // this.name = 'animal' 대신 메서드 사용
    alert('animal');
  }

  constructor() {
    this.showName(); // alert(this.name); 대신 메서드 호출
  }
}

class Rabbit extends Animal {
  showName() {
    alert('rabbit');
  }
}

new Animal(); // animal
new Rabbit(); // rabbit

근데 필드값은 생성자가 끝나고 처리된다고 한거 아닌가?

클래스 필드를 지정할 때 실행 순서가 다음과 같다.

  1. 상속받지 않은 부모 클래스는 생성자 실행 이전에 초기화된다.
  2. 부모 클래스가 있는 경우 super() 실행 직후에 초기화된다.

자바스크립트는 오버라이딩시 필드와 메서드의 동작 방식이 다르다.

이런 문제는 필드를 부모 생성자에서 사용할 때만 발생한다.

super의 동작 과정

단순하게 생각하면 super.method()를 호출하면 this.__proto__.method()를 통해서 부모 객체의 method()를 찾을 수 있을 것 같지만 실제로 super는 이런 방식으로 동작하지 않는다.

let animal = {
  name: "동물",
  eat() {
    alert(`${this.name} 이/가 먹이를 먹습니다.`);
  }
};

let rabbit = {
  __proto__: animal,
  eat() {
    // call을 사용해 컨텍스트를 옮겨가며 부모(animal) 메서드를 호출합니다.
    this.__proto__.eat.call(this); // (*)
  }
};

let longEar = {
  __proto__: rabbit,
  eat() {
    // longEar를 가지고 무언가를 하면서 부모(rabbit) 메서드를 호출합니다.
    this.__proto__.eat.call(this); // (**)
  }
};

longEar.eat(); // RangeError: Maximum call stack size exceeded

위와 같이 했을 때 에러가 나는데 그 이유는 rabbit.eat() 안에서 this가 longEar이기 때문에 무한루프에 빠지기 때문이다.

이런 문제는 this만으로 해결할 수 없기 때문에 특별한 전용 프로퍼티 [[HomeObject]] 가 있다.

[[HomeObject]]

[[HomeObject]] super가 부모 프로토타입과 메서드를 찾는 것을 돕는다.

그런데 [[HomeObject]]를 이용하면 메서드가 객체를 기억하기 때문에 binding이 된 함수를 변경할 수 없다.

대신 [[HomeObject]] 는 super 내부에서만 유효하다.

함수 프로퍼티가 아니라 메서드 형태로 사용해야 [[HomeObject]] 가 설정이 되기 때문에 에러가 발생하지 않는다.

정적 메서드 정적 프로퍼티

프로토타입이 아니라 클래스 함수 자체의 메서드를 static 키워드를 사용해 만들 수 있다.

private, protected 프로퍼티

객체지향에서 가장 중요한 원리 중 하나는 내부와 외부 인터페이스를 구분 짓는 것이다.

자바스크립트에는 두가지 타입의 프로퍼티가 있다.

  • public: 어디서든지 접근이 가능하며 외부 인터페이스를 구성한다.
  • private: 클래스 내부에서만 접근할 수 있고 내부 인터페이스를 구성할 때 쓰인다.

protected 프로퍼티

protected 프로퍼티는 자신과 자손 클래스에서만 접근을 허용한다.

자바스크립트는 protected 필드를 지원하지 않지만 모방해서 사용하는 경우가 많다.

protected 프로퍼티명 앞에는 _ 이 붙여서 사용한다.

private 프로퍼티

private 프로퍼티는 #으로 시작한다. protected 프로퍼티와 달리 private 필드는 언어 자체에 의해서 강제된다.

내장 클래스 확장

배열, 맵 같은 내장 클래스를 확장할 수 있다.

// 메서드 하나를 추가합니다(더 많이 추가하는 것도 가능).
class PowerArray extends Array {
  isEmpty() {
    return this.length === 0;
  }
}

let arr = new PowerArray(1, 2, 5, 10, 50);
alert(arr.isEmpty()); // false

let filteredArr = arr.filter(item => item >= 10);
alert(filteredArr); // 10, 50
alert(filteredArr.isEmpty()); // false

map이나 filter 등을 사용할 때 반환 값이 그냥 배열이 아니라 위에서 만든 PowerArray가 되었다. map이나 filter에서 객체를 구현할 때 객체의 constructor 프로퍼티를 사용하기 때문에 construcotr인 PowerArray에 따라서 객체를 만들 수 있다.

Symbol.species를 이용하면 map, filter 등에서 메서드를 호출할 때 만들어지는 개체의 생성자를 지정할 수 있다.

class PowerArray extends Array {
  isEmpty() {
    return this.length === 0;
  }

  // 내장 메서드는 반환 값에 명시된 클래스를 생성자로 사용합니다.
  static get [Symbol.species]() {
    return Array;
  }
}

let arr = new PowerArray(1, 2, 5, 10, 50);
alert(arr.isEmpty()); // false

// filter는 arr.constructor[Symbol.species]를 생성자로 사용해 새로운 배열을 만듭니다.
let filteredArr = arr.filter(item => item >= 10);

// filteredArr는 PowerArray가 아닌 Array의 인스턴스입니다.
alert(filteredArr.isEmpty()); // Error: filteredArr.isEmpty is not a function

내장 객체와 정적 메서드 상속

내장 객체는 자체 정적 메서드를 갖는다. 일반적으로 한 클래스가 다른 클래스를 상속 받으면 정적 메서드와 그렇지 않은 메서드를 모두 상속 받는다.

그런데 내장 클래스는 정적 메서드를 상속 받지 못한다.

위 다이어그램에서 보면 Date와 Object를 이어주는 링크가 없기 때문에 정적 메서드를 사용하지 못한다.

그냥 프로터타입을 이용해서 쓰면 되는데 정적 메서드를 만든 이유가 뭐지?

믹스인

여러개의 클래스를 상속하고 싶을 때 믹스인을 사용한다.

믹스인은 다른 클래스를 상속 받을 필요 없이 이 클래스들에 구현되어 있는 메서드를 담고 있는 클래스이다.

// 믹스인
let sayHiMixin = {
  sayHi() {
    alert(`Hello ${this.name}`);
  },
  sayBye() {
    alert(`Bye ${this.name}`);
  }
};

// 사용법:
class User {
  constructor(name) {
    this.name = name;
  }
}

// 메서드 복사
Object.assign(User.prototype, sayHiMixin);

// 이제 User가 인사를 할 수 있습니다.
new User("Dude").sayHi(); // Hello Dude!

위의 예제에서처럼 사용할 수 있다. 프로토타입에 믹스인을 넣는 방식으로 사용된다. 따로 문법이 있는 것은 아닌 것 같다.

좋은 웹페이지 즐겨찾기