사실... 생성자 함수를 이해하고 있다면 이 파트는 그저 문법을 이야기하는 것과 거의 비슷하긴 합니다.
따라서, 만약 이 원리가 이해가 안되신다면, 생성자 함수를 다시 이해하는 것을 추천합니다. 😉
일단 문법적 설탕에 대한 정의를 알 필요가 있겠습니다.
wikipedia - syntactic sugar에 의하면, 다음과 정의로 기술되어 있습니다.
In computer science, syntactic sugar is syntax within a programming language that is designed to make things easier to read or to express.
핵심은 easier to read to express
입니다. 더욱 쉽게 읽고 표현하기 위해 사용하는 프로그래밍 언어라는 거죠.
일각에서는, 클래스를 생성자 함수의 문법적 설탕이라고 해석하기도 합니다.
이를 뒷받침하는 근거는 다음과 같습니다.
하지만 단순한 문법적 설탕이라고 하기에는, 클래스는 좀 더 진화된 특성을 가지고 있습니다.
new
연산자 호출에 대해 암묵적으로 strict
합니다.extends
와 super
을 지원한다.따라서 단순히 읽기 쉽고, 표현한 기능만 가지기보다는, 새로운 객체 생성 매커니즘으로 보는 것이 바람직합니다.
클래스는 일급객체이며, 함수입니다.
따라서 익명으로 쓸 수도, 기명으로 사용할 수도 있습니다.
class Person {};
const Person = class {};
const Person = class MyClass {};
클래스는 함수 선언문처럼, 소스코드 평가(런타임 이전)과정에 먼저 평가되어 함수 객체를 생성합니다. 그때 생성된 함수 객체는, 바로 constructor(생성자)
객체입니다.
이때, constructor
는 마치 생성자 함수처럼 호출됩니다. 그렇기에 프로토타입 역시 이때 같이 생성됩니다.
💡 그렇다면, 호이스팅은 어떻게 발생하게 될까요?
답은, 호이스팅이 발생하지만, let
const
처럼 TDZ가 걸려 있게 됩니다.
즉, 실행 컨텍스트 중 LexicalEnvironment
에서 관리한다고 보는 게 타당하겠군요!
const Person = '';
{
console.log(Person);
class Person {}; // Cannot access 'Person' before initialization
}
클래스는 함수이지만, 오로지 인스턴스를 갖기 위한 목적을 갖고 있습니다.
따라서 무조건 new
연산자와 함께 호출되어 인스턴스를 생성합니다.
🙇🏻♂️ 어떻게 보면 너무 당연한 이야기라, 자세한 설명은 생략해도 무방하겠군요!
크게 3가지가 있습니다.
constructor
메서드, 프로토타입 메서드, 정적 메서드입니다.
여기서 핵심은, 어떤 목적을 가졌는지입니다. 이를 이해한다면 각 특성을 이해하기 쉽습니다.
constructor
)인스턴스 생성 및 초기화를 담당합니다.
어떻게 보면, 우리가 생성자 함수 때 살펴보았던 프로토타입의 constructor
과 동일하다고 보이지만, 사실 관련이 없습니다.
실제 결과를 볼까요?
class TestA {
constructor(score) {
this.score = score;
}
}
const testA = new TestA(100);
console.log(testA)
만약 prototype
의 constructor
프로퍼티가 class
의 constructor
메서드와 같았다면, constructor
에는 동적으로 생성자 함수로써 constructor
메서드가 할당되어야 합니다.
하지만 클래스 자체가 constructor
로 주어지는 것을 보니, constructor
은 그저 평가 시 함수 객체를 생성하여 인스턴스를 초기화하기 위한 용도라는 것을 알 수 있습니다.
그리고, 이러한 constructor
은 암묵적으로 빈 객체를 생성하며, 스스로도 생략이 가능합니다. 그렇게 동작시켜도 암묵적으로 빈 constructor
을 스스로 정의합니다.
class A {}
const a = new A();
console.log(a) // A {}
클래스 함수 내에 [[메서드명]](인자) {}
꼴로 작성합니다.
클래스 몸체에서 정의하면, 기본적으로 프로토타입의 메서드로 정의가 됩니다!
이유라면.. 이전에 서술했듯이, 생성자 함수에서 인스턴스의 프로퍼티로 할당하는 것은 인스턴스를 생성할 때마다 추가적인 연산을 주어야 합니다. 이는 비효율적이므로, 동작 자체를 프로토타입으로 인스턴스에 전달함으로써 최적화를 시켜줄 수 있는 것이죠!
아래의 sayHi
는 프로토타입 메서드입니다.
class Person {
constructor() {}
sayHi() {
console.log("Hi!");
}
}
인스턴스에서 호출하는 것이 아닌, 생성자 함수에서 호출할 수 있는 메서드입니다.
static
키워드를 통해 정적 메서드로 변환이 가능합니다.
class Person {
constructor() {}
static sayHi() {
console.log("Hi!");
}
}
Person.sayHi(); // Hi!
둘의 핵심은 인스턴스가 호출의 대상인지를 판별하면 간단합니다.
원리는 함수는 객체라는 것에서부터 시작합니다.
(그렇다면 클래스 역시 이에 속하겠군요! 😮)
함수는 객체이기 때문에 프로퍼티와 메서드를 함수 자체도 가질 수 있는 것이죠.
따라서 인스턴스의 생성 과정에서 함수 자기 자신에 대한 메서드까지 메서드로 할당하지는 않습니다. 그저 this
로 인스턴스가 가질 프로퍼티와 메서드를 정의할 뿐이죠.
즉, 정적 메서드는 인스턴스의 프로퍼티 상속 및 프로토타입 체인에 등록되지 않으며, 오직 생성자 함수에만 메서드로 바인딩되었다고 할 수 있겠네요!
다음과 같은 특징을 지니고 있다고 하네요! 어떻게 보면 지극히 당연합니다.
function
을 생략한 메서드 축약 표현 사용 - 따라서 non-constructor
- new
연산자 호출 불가,
가 필요 없음strict mode
[[Enumerable]] = false
)클래스를 호출하면 [[Construct]]
가 호출됩니다.
(이는 생성자 함수 파트에서 봤듯이, 인스턴스를 호출할 때 사용하는 내부 메서드이죠)
this
바인딩class constructor
은 실제 prototype constructor
과 관련이 없습니다. 따라서
this
가 클래스를 호출할 당시 생성이 되죠.this
의 프로토타입으로 클래스의 prototype
프로퍼티가 가리키는 객체가 바인딩됩니다.this
에 바인딩이 됩니다.class constructor method
가 일할 차례입니다.
만약 생략되어 있다면 이 과정은 건너뜁니다.
constructor
의 내부코드를 실행하면서, this
에 바인딩된 인스턴스를 초기화합니다.constructor
에서 인수로 받은 초기 값을 인스턴스의 프로퍼티 값으로 초기화합니다.this
가 암묵적으로 반환이 됩니다.전술했듯, constructor
에서 정의합니다.
정의된 인스턴스 프로퍼티는 public
의 성격을 가져서, 인스턴스를 통해 언제든지 접근이 가능합니다.
😖 사실상... 생성자 함수의 내용과 비슷해서 생략합니다!
이전에 프로퍼티 어트리뷰트에서 설명했듯, 접근자 프로퍼티는 [[Value]]
라는 게 결여된 친구입니다.
따라서 자체적인 값이 없고, 단지 이름에 걸맞게, 다른 프로퍼티의 값을 읽거나 저장하는 용도입니다.
😖 이 역시, 지금까지 달려오면서 너무나 반복적인 내용이라 생략해도 무방하겠군요.
클래스 기반 객체지향 언어에서, 클래스가 생성할 인스턴스의 프로퍼티를 지칭합니다.
즉, 클래스를 쓸 때 인스턴스의 프로퍼티를 필드라고 할 수 있겠군요!
private
필드 정의 제안이러한 필드는, 아까 인스턴스의 프로퍼티에서 서술했듯 public
한 성격을 가지고 있었습니다.
이는 다른 객체 지향형 언어와 달리 언제든지 접근을 할 수 있다는 측면에서 보안 및 안정성 측면에서 골칫거리였어요. 이러한 배경에서 나온 것이, ES10
의 클래스 필드 정의 제안입니다.
constructor
가 아닌 클래스 몸체에서 인스턴스 프로퍼티 정의일단 이 사양을 구현하기 위해 최신 자바스크립트 엔진들은 미리 선제적으로, 클래스 필드를 클래스 몸체에서도 정의 가능하도록 했습니다.
class Test {
score = 0
}
const test = new Test();
console.log(test); // Test { score: 100 }
이때 주의할 것은, 항상 참조 시 this
를 통해 인스턴스의 프로퍼티로 인식할 수 있게끔 해야 합니다.
class Test {
score = 100
constructor() {
this.weight = 1.5
}
getResult() {
const testResult = this.weight * this.score
return testResult;
}
}
const test = new Test()
console.log(test.getResult()) // 150
private
필드서두가 길었군요.
private
필드에 대해서 이야기를 하자면 간단합니다.
클래스 몸체에서 #
을 붙여서 인스턴스 프로퍼티와 메서드를 정의해주면 끝입니다.
이러한 private
필드는 오직 클래스 내부에서만 참조가 가능하게 됩니다.
class Test {
#score = 100
constructor() {
this.weight = 1.5
}
getResult() {
const testResult = this.weight * this.#score
return testResult;
}
}
const test = new Test()
console.log(test.getResult()) // 150
console.log(test.score) // undefined
static
필드 정의 제안ES12
에서 나온 것으로, 2021년 1월 TC39
에 제안되었다고 합니다.
이를 통해 정적 메서드를 클래스에서 static
키워드로 구현이 가능하게 되었습니다.
class Test {
constructor() {
// 정적 메서드에서 인스턴스의 프로퍼티는 참조할 수 없다. 왜냐하면 인스턴스로 호출하지 않기 때문이다.
this.score = 100
}
static #score = 0
static alertTestStart() {
console.log(`Start Test... now Score: ${Test.#score}`)
}
}
console.log(Test.alertTestStart()) // Start Test... now Score: 0
굉장히 재미있는 파트입니다.
기존의 프로토타입은 생성자 함수 자체가 체인식으로 상속받지 않았는데요!
하지만 상속을 통한 클래스 확장은 클래스 자체가 기존 클래스를 상속 받습니다.
즉, 프로토타입뿐만 아니라, 인스턴스의 프로퍼티, 메서드까지 기존 클래스로부터 상속을 받는 것입니다!
이때, 클래스들을 다음과 같이 부릅니다.
class Test {
constructor(score) {
this.score = score;
this.weights = {
math: 1.5,
english: 1.2
}
}
}
class MathTest extends Test {
getResult() {
return this.score * this.weights.math
}
}
class EnglishTest extends Test {
getResult() {
return this.score * this.weights.english
}
}
const mathTest = new MathTest(100);
const englishTest = new EnglishTest(100);
const totalWeightScore = mathTest.getResult() + englishTest.getResult()
console.log(totalWeightScore) // 270
삼항 연산자를 통해 동적으로 상속 대상을 결정할 수 있는 문법입니다.
실제로 사용해본 적은 없긴 한데...
좋은 코드는 아니지만 다음과 같이 쓸 수 있겠군요!
class SignoPen {
...
static salePrice = 1500;
...
}
class MonamiPen {
...
static salePrice = 2000;
...
}
const needs = {
note: 'morningGlory',
pencil: 'tombow',
pen: 'monami'
}
class Pen extends (needs.pen === 'monami' ? MonamiPen : SignoPen) {}
console.log(Pen.salePrice); // 2000
constructor
너무나 당연한 이야기입니다.
서브 클래스에서 constructor
을 생략하면, 빈 constructor
가 아닌, 상속에 따른 constructor
정의가 이루어지겠죠!
이를 내부 코드로 설명하자면 다음과 같습니다.
constructor(...args) { super(...args) }
super
키워드그렇다면 super
키워드는 무엇일까요?
super
키워드는 식별자처럼 참조도 가능하며, 함수처럼 호출도 가능한 키워드입니다.
호출을 하면, 기본적으로 수퍼클래스의 constructor
을 호출합니다.
그 다음에 다시 constructor
에서 생성할 인스턴스의 프로퍼티를 재정의할 때 사용하는 거죠.
class A {
constructor(a, b) {
this.a = a * 2;
this.b = b * 2;
}
}
class B extends A {
constructor(a, b) {
super(a, b)
this.b = b;
}
}
const b = new B(10, 10);
console.log(b.a, b.b); // 20 10
그런데 주의사항이 있습니다.
만약 이렇게 서브클래스에서 constructor
을 명시한다면, 반드시 super
키워드를 호출해야 합니다.
섀도잉을 피해야 하기 위해서는 super
을 통해 constructor
을 재정의해야 하기 때문입니다.
또한, super
은 항상 서브클래스 인스턴스 프로퍼티 할당보다 우선합니다.
즉, constructor
에서 super
의 호출은 최상단을 권장합니다.
마지막으로 super
은 서브클래스에서만 유효합니다.
만약 서브클래스가 아닌 베이스클래스에서 호출한다면 에러가 발생합니다.
super
참조메서드 내에서 super
를 참조하면, 수퍼클래스의 메서드 호출이 가능합니다.
이때 참조하는 대상은 수퍼클래스의 prototype
프로퍼티에 바인딩된 프로토타입 객체 참조가 가능해야 합니다.
그리고, 만약 해당 인스턴스의 프로퍼티를 해당 수퍼클래스의 메서드에 바인딩하고 싶다면?
그럴 때는 Function.prototype.call
, Function.prototype.apply
를 사용하면 됩니다!
class Milk {
constructor() {
this.price = 1000
}
getSalePrice() {
return this.price * 1.2
}
}
class ChocoMilk extends Milk {
getSalePrice() {
// 지금은 명시적인 this 바인딩이 없으므로 `getSalePrice`의 this는 Milk를 바인딩합니다.
const superPrice = super.getSalePrice.call(this);
return superPrice * 1.5
}
}
const chocoMilk = new ChocoMilk()
console.log(chocoMilk.getSalePrice()) // 1800
super
호출우선적으로 수퍼클래스와 서브클래스를 구분하기 위해 자바스크립트 엔진은 [[ConstructorKind]]
라는 내부 슬롯을 갖게 됩니다.
이때, 수퍼클래스는 base
, 서브클래스는 derived
를 할당합니다.
이때, 수퍼클래스는 기존 인스턴스 생성과정과 동일합니다.
다만 서브클래스는 super
호출이 발생합니다. (암묵적으로도요!)
이후, 수퍼클래스의 constructor
가 호출되면, 수퍼클래스 평가 후 생성된 함수 객체의 코드가 실행됩니다.
즉, 생성 과정의 주체를 수퍼클래스에게 위임한다는 것이 포인트죠!
this
바인딩수퍼클래스가 비록 인스턴스를 생성하지만, this
바인딩은 서브클래스에게 바인딩됩니다.
이유는, 결국 this
바인딩은 호출한 대상에 따라 동적으로 바인딩되는데 new
를 통해 호출한 대상이 서브클래스이기 때문입니다.
constructor
이후에는 this
에 바인딩된 인스턴스를 초기화하게 되겠죠?!
constructor
복귀, this
바인딩원래는 암묵적인 this
가 생성되는 것이 맞지만, 우리는 이 this
의 생성과정을 수퍼클래스에게 위임했습니다.
따라서 이때 생성된 this
는 바로 수퍼클래스가 반환한 인스턴스입니다.
이 this
에 서브클래스는 인스턴스를 바인딩하겠군요! (이것이 클래스 상속이 일어나는 핵심입니다.)
다음부터는 간단합니다.
똑같은 방식으로 인스턴스를 초기화시킵니다.
결과적으로 this
를 반환하겠군요!
어떻게 보면 매우 당연한 개념입니다.
결국 표준 빌트인 생성자 함수 역시 확장할 수 있죠! 따라서 생략합니다.
후... 이 파트 정말 길군요. 약 60쪽을 서술하니 진이 빠지네요. 😭
사실 클래스를 서술하지만, 결국 생성자 함수의 과정이 정말 많이 녹아 있습니다.
이러한 특성 때문에, 클래스가 생성자 함수의 문법적 설탕이라는 말이 나오는 것이겠죠?
그러나, 분명 클래스는 클래스만의 독자적인 강력한 기능들을 제공합니다.
따라서 새로운 객체 생성 방법으로 보는 게 합당하다는 저자의 의견이 납득이 되네요!
사실 이번에, 이전 글들을 보면서 뭔가 스스로 피드백을 해봤습니다.
확실히, 평소에 좀 기분 좋게 글을 써서 그런지, 신나있어 보여서(?) 내용들에 집중이 가질 않더라구요. 😭
좀 더 차분한 호흡으로, 앞으로 공부를 탐구하고 정리해보려 합니다.
다들, 그럼 즐거운 코딩하길 바라며, 이상!