Modern JavaScript Deep Dive 스터디 - CH25 클래스
참고 자료: ⟪모던 자바스크립트 Deep Dive⟫"(이웅모 지음,위키북스, 2020)
new
연산자와 함께 호출new
연산자 없이는 호출 불가new
연산자와 클래스 호출 -> 클래스의 내부 메서드 [[Construct]]
가 호출this
바인딩new
연산자로 클래스를 호출 -> 암묵적으로 빈 객체 생성this
에 바인딩this
에 바인딩되어 있는 인스턴스에 프로퍼티를 추가constructor
가 전달받은 초기값 인수로 인스턴스의 프로퍼티 값을 초기화this
가 암묵적으로 반환constructor
내부에서 정의해야 함constructor
내부에서 this
에 추가한 프로퍼티는 언제나 클래스가 생성한 인스턴스의 프로퍼티가 됨public
class Person {
constructor(name) {
// 인스턴스 프로퍼티
this.name = name; // name 프로퍼티는 public하다.
}
}
const me = new Person("Lee");
// name은 public하다.
console.log(me.name); // Lee
[[Value]]
)을 갖지 않음getter
, setter
로 구성getter
는 반드시 무언가를 반환setter
는 반드시 매개변수가 있어야 함setter
는 단 하나의 값만 할당받기 때문에 단 하나의 매개변수만 선언할 수 있음접근자 프로퍼티
는 인스턴스 프로퍼티가 아닌 프로토타입의 프로퍼티class Person {
constructor(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
// fullName은 접근자 함수로 구성된 접근자 프로퍼티다.
// getter 함수
get fullName() {
return `${this.firstName} ${this.lastName}`;
}
// setter 함수
set fullName(name) {
[this.firstName, this.lastName] = name.split(" ");
}
}
const me = new Person("Ungmo", "Lee");
// 데이터 프로퍼티를 통한 프로퍼티 값의 참조.
console.log(`${me.firstName} ${me.lastName}`); // Ungmo Lee
// 접근자 프로퍼티를 통한 프로퍼티 값의 저장
// 접근자 프로퍼티 fullName에 값을 저장하면 setter 함수가 호출된다.
me.fullName = "Heegun Lee";
console.log(me); // {firstName: "Heegun", lastName: "Lee"}
// 접근자 프로퍼티를 통한 프로퍼티 값의 참조
// 접근자 프로퍼티 fullName에 접근하면 getter 함수가 호출된다.
console.log(me.fullName); // Heegun Lee
// fullName은 접근자 프로퍼티다.
// 접근자 프로퍼티는 get, set, enumerable, configurable 프로퍼티 어트리뷰트를 갖는다.
console.log(Object.getOwnPropertyDescriptor(Person.prototype, "fullName"));
// {get: ƒ, set: ƒ, enumerable: false, configurable: true}
클래스필드
- 클래스 기반 객체지향 언어에서 클래스가 생성할 인스턴스의 프로퍼티를 가리키는 용어
- 자바에서는 마치 클래스 내부에서 변수처럼 사용
- 기존 자바스크립트의 클래스 몸체에는 메서드만 선언 가능 -> 자바와 유사하게 클래스 필드를 선언하면 문법 에러가 발생
class Person {
// 클래스 필드 정의
name = "Lee";
}
const me = new Person();
console.log(me); // Person {name: "Lee"}
Class field declarations
로 인해 인스턴스 프로퍼티를 정의하는 방식은 두 가지가 됨
constuctor에서 인스턴스 프로퍼티를 정의
하는 방식기존 방식
or 클래스 필드 정의 제안
방식특징
this
에 클래스 필드를 바인딩해서는 안됨(this는 constructor와 메서드 내에만 사용 가능) class Person {
// this에 클래스 필드를 바인딩해서는 안된다.
this.name = ''; // SyntaxError: Unexpected token '.'
}
참조
하는 경우, this
를 반드시 사용해야 함class Person {
// 클래스 필드
name = "Lee";
constructor() {
console.log(name); // ReferenceError: name is not defined
}
}
new Person();
undefined
class Person {
// 클래스 필드를 초기화하지 않으면 undefined를 갖는다.
name;
}
const me = new Person();
console.log(me); // Person {name: undefined}
class Person {
// 클래스 필드에 문자열을 할당
name = "Lee";
// 클래스 필드에 함수를 할당
getName = function () {
return this.name;
};
// 화살표 함수로 정의할 수도 있다.
// getName = () => this.name;
}
const me = new Person();
console.log(me); // Person {name: "Lee", getName: ƒ}
console.log(me.getName()); // Lee
#
을 붙임#
을 붙임class Person {
// private 필드 정의
#name = "";
constructor(name) {
// private 필드 참조
this.#name = name;
}
}
const me = new Person("Lee");
// private 필드 #name은 클래스 외부에서 참조할 수 없다.
console.log(me.#name);
// SyntaxError: Private field '#name' must be declared in an enclosing class
class Person {
constructor(name) {
// private 필드는 클래스 몸체에서 정의해야 한다.
this.#name = name;
// SyntaxError: Private field '#name' must be declared in an enclosing class
}
}
접근 가능성 | public | private |
---|---|---|
클래스 내부 | O | O |
자식 클래스 내부 | O | X |
클래스 인스턴스를 통한 접근 | O | X |
static public 필드
, static private 필드
, static private 메서드
를 정의class MyMath {
// static public 필드 정의
static PI = 22 / 7;
// static private 필드 정의
static #num = 10;
// static 메서드
static increment() {
return ++MyMath.#num;
}
}
console.log(MyMath.PI); // 3.142857142857143
console.log(MyMath.increment()); // 11
extends
키워드가 기본적으로 제공class Animal {
constructor(age, weight) {
this.age = age;
this.weight = weight;
}
eat() {
return "eat";
}
move() {
return "move";
}
}
// 상속을 통해 Animal 클래스를 확장한 Bird 클래스
class Bird extends Animal {
fly() {
return "fly";
}
}
const bird = new Bird(1, 5);
console.log(bird); // Bird {age: 1, weight: 5}
console.log(bird instanceof Bird); // true
console.log(bird instanceof Animal); // true
console.log(bird.eat()); // eat
console.log(bird.move()); // move
console.log(bird.fly()); // fly
extends
키워드를 사용하여 상속받은 클래스를 정의서브 클래스
(파생 클래스, 자식 클래스): 상속을 통해 확장된 클래스수퍼 클래스
(베이스 클래스, 부모 클래스): 서브클래스에게 상속된 클래스// 수퍼(베이스/부모)클래스
class Base {}
// 서브(파생/자식)클래스
class Derived extends Base {}
extends
키워드를 통해 생성자 함수를 상속 가능// 생성자 함수
function Base(a) {
this.a = a;
}
// 생성자 함수를 상속받는 서브클래스
class Derived extends Base {}
const derived = new Derived(1);
console.log(derived); // Derived {a: 1}
extends
키워드 다음에는 클래스
, [[Constructor]]
내부 메서드를 갖는 함수 객체로 평가될 수 있는 모든 표현식을 사용 가능function Base1() {}
class Base2 {}
let condition = true;
// 조건에 따라 동적으로 상속 대상을 결정하는 서브클래스
class Derived extends (condition ? Base1 : Base2) {}
const derived = new Derived();
console.log(derived); // Derived {}
console.log(derived instanceof Base1); // true
console.log(derived instanceof Base2); // false
constructor
를 생략하면 암묵적으로 빈 객체가 정의 됨// 수퍼클래스
class Base {
constructor() {}
}
// 서브클래스
class Derived extends Base {
constructor() {
super();
}
}
const derived = new Derived();
console.log(derived); // Derived {}
서브 클래스
에 constructor
를 생략하면 암묵적으로 super(...args)
가 정의 됨constructor(...args) { super(...args); }
// 수퍼클래스
class Base {
constructor(a, b) {
this.a = a;
this.b = b;
}
}
// 서브클래스
class Derived extends Base {
// 다음과 같이 암묵적으로 constructor가 정의된다.
// constructor(...args) { super(...args); }
}
const derived = new Derived(1, 2);
console.log(derived); // Derived {a: 1, b: 2}
super()
는 수퍼클래스의 constructor
를 호출하여 인스턴스를 생성super 호출
-> 수퍼 클래스의 constructor
(super-constructor)를 호출// 수퍼클래스
class Base {
constructor(a, b) {
// ④
this.a = a;
this.b = b;
}
}
// 서브클래스
class Derived extends Base {
constructor(a, b, c) {
// ②
super(a, b); // ③
this.c = c;
}
}
const derived = new Derived(1, 2, 3); // ①
console.log(derived); // Derived {a: 1, b: 2, c: 3}
super 호출
주의 사항
class Base {}
class Derived extends Base {
constructor() {
// ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
console.log("constructor call");
}
}
const derived = new Derived();
class Base {}
class Derived extends Base {
constructor() {
// ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
this.a = 1;
super();
}
}
const derived = new Derived(1);
class Base {
constructor() {
super(); // SyntaxError: 'super' keyword unexpected here
}
}
function Foo() {
super(); // SyntaxError: 'super' keyword unexpected here
}
super 참조
-> 수퍼클래스의 메서드를 호출 할 수 있음
// 수퍼클래스
class Base {
constructor(name) {
this.name = name;
}
sayHi() {
return `Hi! ${this.name}`;
}
}
// 서브클래스
class Derived extends Base {
sayHi() {
// super.sayHi는 수퍼클래스의 프로토타입 메서드를 가리킨다.
return `${super.sayHi()}. how are you doing?`;
}
}
const derived = new Derived("Lee");
console.log(derived.sayHi()); // Hi! Lee. how are you doing?
// 수퍼클래스
class Base {
constructor(name) {
this.name = name;
}
sayHi() {
return `Hi! ${this.name}`;
}
}
class Derived extends Base {
sayHi() {
// __super는 Base.prototype을 가리킨다.
const __super = Object.getPrototypeOf(Derived.prototype);
return `${__super.sayHi.call(this)} how are you doing?`;
}
}
/*
[[HomeObject]]는 메서드 자신을 바인딩하고 있는 객체를 가리킨다.
[[HomeObject]]를 통해 메서드 자신을 바인딩하고 있는 객체의 프로토타입을 찾을 수 있다.
예를 들어, Derived 클래스의 sayHi 메서드는 Derived.prototype에 바인딩되어 있다.
따라서 Derived 클래스의 sayHi 메서드의 [[HomeObject]]는 Derived.prototype이고
이를 통해 Derived 클래스의 sayHi 메서드 내부의 super 참조가 Base.prototype으로 결정된다.
따라서 super.sayHi는 Base.prototype.sayHi를 가리키게 된다.
*/
super = Object.getPrototypeOf([[HomeObject]])
(1) 서브클래스의 super 호출
(2) 수퍼클래스의 인스턴스 생성과 this 바인딩
(3) 수퍼클래스의 인스턴스 초기화
(4) 서브클래스 constructor로의 복귀와 this 바인딩
(5) 서브클래스의 인스턴스 초기화
(6) 인스턴스 반환
// 수퍼클래스
class Rectangle {
constructor(width, height) {
this.width = width;
this.height = height;
}
getArea() {
return this.width * this.height;
}
toString() {
return `width = ${this.width}, height = ${this.height}`;
}
}
// 서브클래스
class ColorRectangle extends Rectangle {
constructor(width, height, color) {
super(width, height);
this.color = color;
}
// 메서드 오버라이딩
toString() {
return super.toString() + `, color = ${this.color}`;
}
}
const colorRectangle = new ColorRectangle(2, 4, "red");
console.log(colorRectangle); // ColorRectangle {width: 2, height: 4, color: "red"}
// 상속을 통해 getArea 메서드를 호출
console.log(colorRectangle.getArea()); // 8
// 오버라이딩된 toString 메서드를 호출
console.log(colorRectangle.toString()); // width = 2, height = 4, color = red
[[Construct]]
내부 메서드를 갖는 생성자 함수 -> extends
키워드를 통해 확장 가능// Array 생성자 함수를 상속받아 확장한 MyArray
class MyArray extends Array {
// 중복된 배열 요소를 제거하고 반환한다: [1, 1, 2, 3] => [1, 2, 3]
uniq() {
return this.filter((v, i, self) => self.indexOf(v) === i);
}
// 모든 배열 요소의 평균을 구한다: [1, 2, 3] => 2
average() {
return this.reduce((pre, cur) => pre + cur, 0) / this.length;
}
}
const myArray = new MyArray(1, 1, 2, 3);
console.log(myArray); // MyArray(4) [1, 1, 2, 3]
// MyArray.prototype.uniq 호출
console.log(myArray.uniq()); // MyArray(3) [1, 2, 3]
// MyArray.prototype.average 호출
console.log(myArray.average()); // 1.75