1. 클래스의 인스턴스 생성과정
- 인스턴스 생성과 this 바인딩: 암묵적으로 빈 객체가 생성되고 이 빈 객체가 바로 인스턴스가 된다. 인스턴스는 this에 바인딩 된다.
- 인스턴스 초기화: constructor 내부의 코드가 실행되어 this에 바인딩 되어 있는 인스턴스를 초기화한다.
- 인스턴스 반환: 완성된 인스턴스가 바인딩된 this가 암묵적으로 반환된다.
2. 프로퍼티
- 인스턴스 프로퍼티: constructor 내부에서 정의해야한다. 앞에서 설명했으니 넘어간다.
- 접근자 프로퍼티: 값이 존재하지 않고 다른 프로퍼티의 값을 읽거나 저장할 때 사용하는 접근자 함수(getter, 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}
클래스 안에서 선언한 메서드는 기본적으로 프로토타입 메서드가 되기 때문에 인스턴스의 프로퍼티가 아닌 프로토타입의 프로퍼티가 된다.(저번 게시글의 예시 참조)
3. 클래스 필드 정의
클래스 필드는 클래스가 생성할 인스턴스의 프로퍼티를 가리키는 용어다. 자바스크립트에서는 인스턴스 프로퍼티를 선언하고 초기화하려면 반드시 constructor 내부에서 this에 프로퍼티를 추가해야한다.
하지만 최신 브라우저와 최신 Node.js에서는 클래스 필드를 클래스 몸체에 정의가 가능하다(주의: ECMAScript의 정식 표준 사양은 아니다.)
Class Person {
//클래스 필드 정의
name = 'Lee';
}
const me = new Person();
console.log(me);
단, 위와 같이 할 경우 this에 클래스 필드를 바인딩해서는 안된다.
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}
함수를 클래스 필드에 할당할 수도 있다. 다만 함수를 할당하면 그 함수는 프로토타입 메서드가 아닌 인스턴스 메서드가 된다. 모든 클래스 필드는 인스턴스 프로퍼티가 되기 때문이다.
결론적으로 인스턴스 프로퍼티를 정의하는 방법은 크게 2가지가 존재한다. 첫째는 이전에 배운 constructor에서 인스턴스 프로퍼티를 초기화하는 방법이고, 두번째는 지금 배운 클래스 필드를 사용하는 것이다.
4. private 필드 정의 제안 단계
ES6에서 클래스는 protected 같은 접근 제한자를 지원하지 않기 때문에 인스턴스 프로퍼티는 언제나 public이다. 하지만 접근제한자는 최신 브라우저와 Node.js에서 이미 구현되어 있다.
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
private 필드는 클래스 내부에서만 참조가 가능하다. 따라서 접근자 프로퍼티를 통해 간접적으로 접근하는 방법만 존재한다. 또한 private 필드는 반드시 클래스 몸체에 정의해야한다. constructor에 정의하면 안된다!
5. static 필드 정의 제안
이전 시간에 static을 사용해서 정적 메서드는 정의가 가능했는데 정적 필드를 정의할 수는 없었다. 하지만 정적 필드가 최신 브라우저와 Node.js에서 구현되어 있다.
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
6. 상속에 의한 클래스 확장
상속에 의한 클래스 확장은 기존 클래스를 상속받아 새로운 클래스를 확장하여 정의하는 것이다. 생성자 함수 방식은 이런 확장 기능이 존재하지 않는다.
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
상속을 통해 확장된 클래스를 서브 클래스라고 하고, 상속된 클래스를 수퍼클래스라고 한다. 명칭은 다양한데 이렇게 알고 있으면 될 듯 싶다. 위 그림처럼 클래스도 프로토타입을 통해 상속 관계를 구현한다. 즉, 인스턴스 프로토타입 체인뿐 아니라 클래스 간의 프로토타입 체인도 생성한다. 이는 프로토타입 메서드, 정적 메서드(클래스에 있는 메서드) 모두 상속이 가능하다는 것을 의미한다.
7. 동적 상속
생성자 함수도 extends로 확장이 가능하다. 다만 extends 앞에는 반드시 클래스가 와야한다.
// 생성자 함수
function Base(a) {
this.a = a;
}
// 생성자 함수를 상속받는 서브클래스
class Derived extends Base {}
const derived = new Derived(1);
console.log(derived); // Derived {a: 1}
동적으로 상속 받을 대상을 결정할 수 있다.
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
8. 서브클래스에서 constructor를 생략하면 클래스에 다음과 같은 constructor가 암묵적으로 정의된다. args는 new 연산자와 함께 클래스를 호출할 때 전달한 인수 리스트다. super는 수퍼 클래스의 constructor를 호출하여 인스턴스를 생성한다.
// 수퍼클래스
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}
9. super 키워드
9-1. super를 호출하면 수퍼클래스의 constructor를 호출한다.
아래 예제를 보면 new 연산자와 함께 서브클래스를 호출하면서 전달한 인수는 모두 super 호출을 통해 수퍼클래스의 constructor에게 전달된다.
주의점은 아래와 같다
- 서브 클래스에서 constructor를 사용하려면 반드시 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();
- 서브 클래스의 constructor에서 super를 호출하기 이전에는 this를 참조할 수 없다.
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);
- super는 반드시 서브 클래스의 constructor에서만 호출한다.
class Base {
constructor() {
super(); // SyntaxError: 'super' keyword unexpected here
}
}
function Foo() {
super(); // SyntaxError: 'super' keyword unexpected here
}
9-2. 메서드 내에서 super를 참조하면 수퍼클래스의 메서드를 호출할 수 있다.
- 서브 클래스의 프로토타입 메서드 내에서 super.sayHi는 수퍼클래스의 프로토타입 메서드 sayHi를 가리킨다
// 수퍼클래스
class Base {
constructor(name) {
this.name = name;
}
sayHi() {
return `Hi! ${this.name}`;
}
}
class Derived extends Base {
sayHi() {
return `$super.sayHi()} how are you doing?`;
}
}
- 서브클래스의 정적 메서드 내에서 super.sayHi는 수퍼클래스의 정적 메서드 sayHi를 가리킨다.
// 수퍼클래스
class Base {
static sayHi() {
return `Hi! ${this.name}`;
}
}
class Derived extends Base {
static sayHi() {
return `${super.sayHi()} how are you doing?`;
}
}
상속 클래스의 인스턴스 생성 과정
1. 서브클래스의 super 호출.
자바스크립트 엔진은 수퍼 클래스와 서브 클래스 구분을 위해 내부 슬롯 [[ConstructorKind]]를 갖는다. 아무것도 상속받지 않는 클래스는 “base”로, 상속을 받는 클래스는 “derived”로 설정된다. 서브클래스는 자신이 직접 인스턴스를 생성하지 않고 수퍼클래스에게 인스턴스 생성을 위임한다. 이것이 바로 서브클래스의 constructor에서 반드시 super를 호출해야하는 이유다.
2. 수퍼클래스의 인스턴스 생성과 this 바인딩
constructor 내부의 코드가 실행되기 이전에 암묵적으로 빈 객체를 생성하고 인스턴스는 this에 바인딩 된다.
주의할 점은 인스턴스는 수퍼클래스가 만들지만 new.target(new 연산자와 함께 호출된 함수를 가리키는)은 서브클래스를 가리킨다. 이는 인스턴스는 서브클래스가 생성한 것으로 처리하지만 실상은 수퍼클래스가 생성한다는 뜻이다. 하지만 서브클래스가 생성한 것으로 처리하기 때문에 생성된 인스턴스의 프로토타입은 수퍼클래스의 프로토타입이 아니라 서브클래스의 프로토타입을 가리킨다.
// 수퍼클래스
class Rectangle {
constructor(width, height) {
// 암묵적으로 빈 객체, 즉 인스턴스가 생성되고 this에 바인딩된다.
console.log(this); // ColorRectangle {}
// new 연산자와 함께 호출된 함수, 즉 new.target은 ColorRectangle이다.
console.log(new.target); // ColorRectangle
// 생성된 인스턴스의 프로토타입으로 ColorRectangle.prototype이 설정된다.
console.log(Object.getPrototypeOf(this) === ColorRectangle.prototype); // true
console.log(this instanceof ColorRectangle); // true
console.log(this instanceof Rectangle); // true
...
3. 수퍼클래스의 인스턴스 초기화
수퍼클래스 내부의 constructor 초기화 로직의 실행으로 인스턴스가 초기화된다.
4. 서브클래스 constructor로의 복귀와 this 바인딩
서브클래스는 별도의 인스턴스를 생성하지 않고 super가 반환한 인스턴스를 this에 바인딩하여 그대로 사용한다.
// 서브클래스
class ColorRectangle extends Rectangle {
constructor(width, height, color) {
super(width, height);
// super가 반환한 인스턴스가 this에 바인딩된다.
console.log(this); // ColorRectangle {width: 2, height: 4}
...
즉, super를 호출하지 않는다면 인스턴스가 생성되지 않으며, this 바인딩도 불가하다. 서브클래스에서 반드시 super를 호출해야하는 이유가 바로 이것이다.
5. 서브클래스의 인스턴스 초기화
서브클래스의 constructor 인스턴스 초기화 로직이 실행된다.
6. 인스턴스 반환
완성된 인스턴스가 바인딩된 this가 암묵적으로 반환된다.
// 서브클래스
class ColorRectangle extends Rectangle {
constructor(width, height, color) {
super(width, height);
// super가 반환한 인스턴스가 this에 바인딩된다.
console.log(this); // ColorRectangle {width: 2, height: 4}
// 인스턴스 초기화
this.color = color;
// 완성된 인스턴스가 바인딩된 this가 암묵적으로 반환된다.
console.log(this); // ColorRectangle {width: 2, height: 4, color: "red"}
}
참고로 표준 빌트인 함수들도 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
'개발 관련 도서' 카테고리의 다른 글
모던 자바스크립트 - 배열(2) (0) | 2024.06.23 |
---|---|
모던 자바스크립트 - 배열(1) (1) | 2024.06.18 |
모던 자바스크립트 - 클래스(1) (0) | 2024.06.10 |
모던 자바스크립트 - 실행 컨텍스트(2) (0) | 2024.05.28 |
모던 자바스크립트 - 실행 컨텍스트(1) (0) | 2024.05.21 |