728x90
반응형
클래스 == 새로운 객체 생성 매커니즘의 “함수”
- 클래스와 생성자 함수의 차이
- 클래스를 new 연산자 없이 호출하면 에러가 발생 생성자 함수는 new 없이 호출해도 일반 함수로 호출됨
- 클래스는 상속을 지원하는 extends와 super 키워드를 제공
- 클래스는 let과 const처럼 호이스팅이 발생하지 않는 것 처럼 동작
- 클래스 내의 모든 코드에는 암묵적으로 strict mode가 지정되어 실행되면 해제 불가
- 클래스의 constructor, 프로토타입 메서드, 정적 메서드는 모두 프로퍼티 어트리뷰트 환경[[Enumerable]] 값이 모두 false === 열거 안됌.
클래스를 프로토타입 기반 객체 생성 패턴의 단순한 문법적 설탕이라고 보기보다는 새로운 객체 생성의 메커니즘으로 보는 것이 좀 더 합당
클래스의 정의
// 클래스 선언문
class Person {}
// 익명 클래스 표현식
const Person = class {};
// 기명 클래스 표현식
const Person = class MyClass {};
- 클래스를 표현식으로 정의한다는 것 --> 일급 객체
일급객체의 특징: 무명의 리터럴로 생성, 변수나 자료구조에 저장, 함수의 매개변수에 전달, 함수의 반환값으로 사용 가능
클래스 호이스팅
- 클래스는 클래스 정의 이전에 참조 불가--> let, const 키워드로 선언한 변수 처럼 호이스팅 (호이스팅 되지 않는 것처럼 동작, 일시적 사각지대 존재)
인스턴스 생성
- 반드시 new 연산자와 함께 호출 필수
- 클래스 표현식으로 만들어진 클래스인 경우 기명 클래스 표현식의 클래스 이름이 아닌 식별자를 사용해 인스턴스를 생성 필요
메서드
- 클래스 몸체에는 constructor, 프로토타입 메서드, 정적 메서드 이 3 가지의 메서드만 정의 가능
class Person {
constructor (name) {
this.name = name;
}
sayHi() {
console.log(`Hi! My name is ${this.name}`);
}
static sayHello() {
console.log('Hello!');
}
}
const me = new Person('Lee');
console.log(me.name);
me.sayHi();
Person.sayHello();
constructor
- 인스턴스를 생성하고 초기화하기 위한 특수한 메서드
- 클래스의 constructor 메서드와 프로토타입의 constructor 프로퍼티는 상이
- 클래스의 constructor는 최대 1개만 존재할 수 있으며 생략 가능 생략한 경우 빈 constructor가 암묵적으로 정의되고, 이는 빈 객체를 생성
- 인스턴스를 초기화하려면 constructor를 생략해서는 안되고 초기값을 매개변수로 전달해서 사용
- return문 반드시 생략 필수
프로토타입 메서드
- 클래스 몸체에서 메서드 축약 표현으로 정의하면 인스턴스의 프로토타입에 존재하는 프로토타입 메서드가 됨.
정적 메서드
- 클래스 몸체에서 static 키워드를 붙이면 정적 메서드가 됨.
정적(Static) 메서드 : 인스턴스를 생성하지 않아도 호출할 수 있는 메서드
정적 메서드와 프로토타입 메서드의 차이
- 자신이 속해 있는 프로토타입 체인이 상이
- 정적 메서드는 클래스로 호출하고 프로토타입 메서드는 인스턴스로 호출
- 정적 메서드는 인스턴스 프로퍼티를 참조할 수 없지만 프로토타입 메서드는 인스턴스 프로퍼티를 참조 가능
- 표준 빌트인 객체 Math, Number, JSON, Object, Reflect 또한 다양한 정적 메서드를 소유
- 이처럼 클래스 또는 생성자 함수를 하나의 네임스페이스로 사용하여 정적 메서드를 모아 놓으면 이름 충돌 가능성을 줄여주고 관련 함수들을 구조화할 수 있는 효과 보유
클래스에서 정의한 메서드의 특징
- function 키워드를 생략한 메서드 축약 표현을 사용
- 객체 리터럴과는 다르게 클래스에 메서드를 정의할 때는 콤마가 필요 없음.
- 암묵적 strict mode로 실행
- for...in 문이나 Object.keys 메서드 등으로 열거 불가. ([[Enumerable]] 값이 false)
- 내부 메서드 [[Construct]]를 갖지 않는 non-constructor 따라서 new 연산자와 함께 호출 불가
클래스의 인스턴스 생성 과정
- 인스턴스 생성과 this 바인딩 (빈 객체 생성하는데 이것이 클래스가 생성한 인스턴스)
- 인스턴스 초기화
- 인스턴스 반환
class Person {
// 생성자
constructor(name) {
// 1. 암묵적으로 인스턴스가 생성되고 this에 바인딩
console.log(this); // Person {}
console.log(Object.getPrototypeOf(this) === Person.prototype); // true
// 2. this에 바인딩되어 있는 인스턴스 초기화
this.name = name;
// 3. 완성된 인스턴스가 바인딩된 this를 암묵적 반환
}
}
프로퍼티
인스턴스 프로퍼티
- 인스턴스 프로퍼티는 언제나 constructor 내부에서 정의 필수
접근자 프로퍼티
- getter 함수와 setter 함수가 있으며 클래스의 메서드가 기본적으로 프로토타입 메서드가 되므로 클래스의 접근자 프로퍼티 또한 인스턴스 프로퍼티가 아닌 프로토타입의 프로퍼티가 됨.
- 이 두 접근자 프로퍼티의 이름은 인스턴스 프로퍼티처럼 사용되는데, 호출하는 것이 아닌 참조하는 형식으로 사용
클래스 필드 정의 제안
class Person {
// 클래스 필드 정의
name = 'Lee';
}
const me = new Person('Lee');
클래스 몸체에서 클래스 필드를 정의할 수 있는 이 제안은 아직 정식 표준사양은 아니지만 미리 몇몇 최신 브라우저와 Node.js에서 구현해 놓아서 사용 가능
원래는 인스턴스 프로퍼티를 참조하려면 this를 사용하여 참조를 해야하지만 이 클래스 필드는 this를 생략해도 참조 가능
- this에 클래스 필드를 바인딩 해서는 안되며 this는 클래스의 constructor와 메서드 내에서만 유효
- 클래스 필드에 메서드또한 정의할 수 있는데 이 메서드는 인스턴스 프로퍼티가 됨.
- 모든 클래스 필드는 인스턴스 프로퍼티가 되기 때문에 따라서 클래스 필드에 함수를 할당하는 것은 권장 X
화살표 함수와 클래스
<!DOCTYPE html>
<html>
<body>
<button class="btn">0</button>
<script>
class App {
constructor() {
this.$button = document.querySelector('.btn');
this.count = 0;
// increase 메서드를 이벤트 핸들러로 등록
// 이벤트 핸들러 increase 내부의 this는 DOM 요소(this.$button)를 가리킨다.
// 하지만 increase는 화살표 함수로 정의되어 있으므로
// increase 내부의 this는 인스턴스를 가리킨다.
this.$button.onclick = this.increase;
// 만약 increase가 화살표 함수가 아니라면 bind 메서드를 사용해야 한다.
// $button.onclick = this.increase.bind(this);
}
// 인스턴스 메서드
// 화살표 함수 내부의 this는 언제나 상위 컨텍스트의 this를 가리킨다.
increase = () => this.$button.textContent = ++this.count;
}
new App();
</script>
</body>
</html>
private 필드 정의 제안
- #을 붙여서 private 필드를 참조 가능
class Person {
#name = ' ';
constructor(name) {
this.#name = name;
}
}
반드시 클래스 몸체에 정의하며, constructor에 정의하면 에러가 발생
- 필드 참조 여부
접근 가능성 public private 클래스 내부 O O 자식 클래스 내부 O X 클래스 인스턴스를 통한 접근 O X
static 필드 정의 제안
- static 키워드를 사용하여 정적 필드 정의
class MyMath {
// static public 필드 정의
static PI = 22/7;
// static private 필드 정의
static #num = 10;
// static 메서드
static increment() {
return ++MyMath.#num;
}
}
console.log(MyMath.PI);
console.log(MyMath.increment()); // 11
상속에 의한 클래스 확장
클래스 상속과 생성자 함수 상속
- 상속에 의한 클래스 확장은 기존 클래스를 상속받아 새로운 클래스를 확장하여 정의하는 것
extends 키워드
- 상속을 통해 확장하려면 extends키워드를 사용하여 클래스 정의
- 서브 클래스 : 상속을 통해 확장된 클래스
- 수퍼 클래스 : 서브클래스에게 상속된 클래스
동적 상속
- extends키워드는 클래스뿐만 아니라 생성자 함수를 상속받아 클래스 확장 가능
- 클래스뿐만 아니라 [[Consturct]] 내부 메서드를 갖는 함수 객체로 평가될 수 있는 모든 표현식 사용 가능
- 이를 통해 동적으로 상속 받는 대상을 결정 가능
서브클래스의 constructor
- 서브클래스에서 constructor를 생략하면 다음과 같이 암묵적 정의
constructor(...arge) { super(...args); }
super 키워드
- super : 함수처럼 호출할 수도 있고 this와 같이 식별자처럼 참조할 수 있는 특수한 키워드
- super를 호출하면 수퍼클래스의 constructor를 호출
- super를 참조하면 수퍼클래스의 메서드 호출 가능
super 호출
- super를 호출하면 수퍼클래스의 constructor를 호출
- 수퍼클래스에서 추가한 프로퍼티와 서브클래스에서 추가한 프로퍼티를 갖는 인스턴스를 생성한다면 constructor 생략 불가
- 따라서 이경우 서브클래스의 constructor에는 반드시 super를 호출 필요
- 서브클래스의 constructor에서 super를 호출하기 전에는 this를 참조 불가
- super는 반드시 constructor에서만 호출
super 참조
- 메서드 내에서 super를 참조하면 수퍼클래스의 메서드 호출 가능
- 서브클래스의 메서드 내에서의 super.메서드는 수퍼클래스의 프로토타입 메서드를 지침
- super가 아닌 getPrototypeOf로 super를 정의하여 수퍼클래스의 메서드를 호출할 경우, call(this)를 붙여서 사용 필요 ← 프로토타입 메서드이기 때문에 this는 인스턴스를 가리켜야 하기 때문
- 따라서 메서드는 내부 슬롯 [[HomeObject]]를 가지며 이는 자신을 바인딩하고 있는 객체를 지침
- 클래스 뿐만 아니라 객체 리터럴에서 메서드 축약표현으로 정의된 함수 또한 super 참조 사용 가능
// super 참조 의사코드
super = Object.getPrototypeOf([[HomeObject]])
ES6 메서드 축약표현으로 정의된 함수만이 [[HomeObject]]를 소유
- 서브클래스의 정적 메서드 내에서 super.sayHi는 수퍼클래스의 정적 메서드 sayHi를 지침
상속 클래스의 인스턴스 생성 과정
- Rectangle클래스와 이를 확장한 ColorRectangle클래스의 인스턴스 생성 과정
// 수퍼클래스
// Rectangle클래스와 이를 확장한 ColorRectangle클래스
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
- 서브 클래스 super 호출
- JS엔진은 수퍼클래스와 서브클래스를 구분하기 위해 'base' 또는 'derived' 값을 갖는 내부 슬롯 [[ConstructorKind]]를 갖는다. 상속받는 서브 클래스를 derived로, 상속받지 않는 클래스는 base로 설정
- 서브클래스의 constructor에서 반드시 super를 호출해야 하는 이유
- new 연산자와 함게 호출될때, 서브클래스는 자신이 직접 인스턴스를 생성하지 않고 수퍼클래스에게 인스턴스 생성을 위임하기 때문
- 수퍼클래스의 인스턴스 생성과 this 바인딩
- new 연산자와 함께 호출된 클래스가 서브클래스이기 때문에 실질적으로는 colorRectangle가 생성할 인스턴스에 this가 바인딩됨.
// 수퍼클래스 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 ... // 수퍼클래스 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 ...
- new 연산자와 함께 호출된 클래스가 서브클래스이기 때문에 실질적으로는 colorRectangle가 생성할 인스턴스에 this가 바인딩됨.
- 수퍼클래스의 인스턴스 초기화
- this에 바인딩되어 있는 인스턴스 초기화
- this에 바인딩 되어 있는 인스턴스에 프로퍼티를 추가하고 constructor가 인수로 전달받은 초기값으로 인스턴스 프로퍼티를 초기화시킴
// 수퍼클래스 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 // 인스턴스 초기화 this.width = width; this.height = height; console.log(this); // ColorRectangle {width: 2, height: 4} } ...
- 서브클래스 constructor로의 복귀와 this 바인딩
- super가 반환한 인스턴스가 this에 바인딩
- 서브클래스는 별도의 인스턴스를 생성하지 않고 super가 반환한 인스턴스를 this에 바인딩하여 그대로 사용
// 서브클래스 class ColorRectangle extends Rectangle { constructor(width, height, color) { super(width, height); // super가 반환한 인스턴스가 this에 바인딩 console.log(this); // ColorRectangle {width: 2, height: 4} ...
- 서브클래스의 인스턴스 초기화
- 인스턴스 반환
// 서브클래스
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"}
}
...
표준 빌트인 생성자 함수 확장
- [[Constructor]] 내부 메서드를 갖는 함수 객체로 평가될 수 있는 모든 표현식을 extends 다음 키워드로 사용할 수 있기 때문에 String, Number, Array 같은 표준 빌트인 객체도 확장 가능
- Array를 확장하여 사용하는 경우 Array.prototype의 모든 메서드들을 사용할 수 있고 이를 통해 반환하는 메서드가 클래스가 MyArray라면 이 클래스의 인스턴스를 반환
- 이를 통해 메서드 체이닝이 가능하며 만약 MyArray 클래스의 메서드가 인스턴스가 아닌 Array가 생성한 인스턴스를 반환하게 하려면 Symbol.species를 사용하여 정적 접근자 프로퍼티를 추가
class MyArray extends Array {
static get [Symbol.species]() { return Array; }
uniq() {
return this.filter((v, i , self) => self.indexOf(v) === i);
}
average() {
return this.reduce((pre, cur) => pre + cur, 0 ) / this.length;
}
}
반응형
'언어 > JavaScript Deepdive' 카테고리의 다른 글
24장 : 배열 (0) | 2024.04.02 |
---|---|
23장 : ES6 함수 추가 기능 (0) | 2024.04.02 |
21장 : 클로저 (0) | 2024.04.02 |
20장 : 실행 컨텍스트 (0) | 2024.04.01 |
19장 : this (0) | 2024.04.01 |