클래스
같은 프로토타입 객체에서 프로퍼티를 상속하는 객체 집합.
단순한 자바스크립트 클래스
- 위 코드는 Range 객체를 생성하는 팩토리 함수 range()를 정의한다.
- range() 함수는 methods 프로퍼티에 클래스를 정의하는 프로토타입 객체를 저장한다.
- range() 함수는 Range 객체에 from과 to 프로퍼티를 정의한다. 이들은 공유되지 않고 상속되지 않는 프로퍼티이며, 각 Range 객체의 고유한 상태를 나타낸다.
- range.methods 객체는 ES6 단축 문법을 사용해 메서드를 정의했다.
- Symbol.iterator 메서드는 Range 객체의 이터레이터를 정의한다. 메서드 이름 앞에 있는 *는 이것이 일반적인 함수가 아니라 제너레이터 함수라는 것을 의미한다.
- range.methods에 정의된 '공유, 상속되는 메서드'는 모두 range() 팩토리 함수에서 초기화한 from과 to 프로퍼티를 사용한다. 메서드는 this 키워드를 사용해 자신이 호출된 객체를 참조하는 방식으로 해당 프로퍼티에 접근한다.
9.2 클래스와 생성자
생성자: 새로 생성된 객체를 초기화하도록 설계된 함수
생성자는 new 키워드를 사용해 호출한다.
(new 키워드가 생성자인 것이 아님)
생성자 호출에서 중요한 특징은 생성자의 prototype 프로퍼티가 새 객체의 프로토타입으로 사용된다는 것
생성자를 사용한 Range 클래스
// 이 함수는 객체를 생성하거나 반환하지 않는다. 그저 초기화할 뿐
function Range(from, to) {
this.from = from;
this.to = to;
}
Range.prototype = {
includes: function(x) { return this.from <= x && x <= this.to; },
[Symbol.iterator]: function*() {
for (let x = Math.ceil(this.from); x <= this.to; x++) yield x;
},
toString: function() { return "(" + this.from + " ~ " + this.to + ")"; }
};
let fromAtoB = new Range(1, 3);
fromAtoB.includes(2) // true
fromAtoB.toString() // "(1 ~ 3)"
console.log([...fromAtoB]) // [1, 2, 3]
클래스 이름은 대문자로 시작한다.
일반적인 함수와 메서드 이름은 소문자로 시작한다.
Range() 생성자를 new 키워드와 함께 호출하면
새 객체는 생성자를 호출하기 전에 자동으로 생성되고, this 값을 통해 접근할 수 있다.
Range() 생성자는 this를 초기화하기만 하면 된다.
생성자를 호출하면 자동으로 새 객체가 생성되고, 생성자를 그 객체의 메서드로 호출하며 새 객체를 반환한다.
화살표 함수는 prototype 프로퍼티가 없으므로 생성자로 사용할 수 없다.
화살표 함수는 호출한 객체가 아니라 자신이 정의된 컨텍스트에서 this 키워드를 상속한다.
생성자, 클래스의 본질
클래스의 본질은 프로토타입 객체다. 두 객체가 같은 프로토타입 객체를 상속하지 않는다면 같은 클래스의 인스턴스가 아니다.
생성자 함수는 그렇지 않다. 서로 다른 생성자 함수의 prototype 프로퍼티가 같은 프로토타입 객체를 참조할 수도 있다.
그리고 두 생성자가 같은 클래스의 인스턴스를 초기화할 수 있다.
생성자 프로퍼티
일반적인 자바스크립트 함수는 모두 생성자로 사용될 수 있고, 생성자를 호출할 때는 prototype 프로퍼티가 필요하다.
따라서 일반적인 자바스크립트 함수는 모두 prototype 프로퍼티를 갖는다. 이 프로퍼티의 값은 열거 불가한 constructor 프로퍼티 단 하나다. constructor 프로퍼티의 값은 함수 객체다.
let F = function () {} // 함수 객체
let p = F.prototype; // F에 연결된 프로토타입 객체
let c = p.constructor; // 프로토타입에 연결된 함수
c === F // => true: 모든 F에 대해 F.prototype.constructor === F
'생성자를 사용한 Range 클래스' 예시에서 정의한 Range 클래스는,
미리 정의된 Range.prototype 객체를 자신이 생성한 객체로 덮어쓰면서 constructor 프로퍼티를 기재하지 않았다.
그렇기 때문에 Range 클래스가 정의하는 새 프로토타입 객체에는 constructor 프로퍼티가 없다. 프로토타입에 생성자를 명시적으로 추가해서 이 문제를 해결할 수 있다.
Range.prototype = {
constructor: Range, // 생성자 역참조를 직접 만들기
// 다른 메서드 생략
}
9.3 class 키워드를 사용하는 클래스
ES6에서 class 키워드를 도입하면서 마침내 고유의 문법이 생겼다.
class Range {
constructor(from, to) {
this.from = from;
this.to = to;
}
includes(x) { return this.from <= x && x <= this.to; }
*[Symbol.iterator] () {
for(let x = Math.ceil(this.from); x <= this.to; x++) yield x;
}
toString() { return `(${this.from} ~ ${this.to})`; }
}
let fromAtoB = new Range(1, 3);
fromAtoB.includes(2) // true
fromAtoB.toString() // "(1 ~ 3)"
console.log([...fromAtoB]) // [1, 2, 3]
- constructor 키워드는 클래스의 생성자 함수를 정의한다. 하지만 정의된 함수에 실제로 'constructor'라는 이름을 쓰지는 않는다. class 선언문은 새 변수 Range를 정의하고 constructor 함수의 값을 그 변수에 할당한다.
다른 클래스를 상속하는 서브클래스를 정의할 때는 class 키워드와 함께 extends 키워드를 사용한다.
// Span은 Range와 비슷하지만 from과 to가 아니라 start와 length로 초기화된다.
class Span extends Range {
constructor(start, length) {
if (length >= 0) {
super(start, start + length);
} else {
super(start + length, start);
}
}
}
함수 선언과 달리 클래스 선언은 끌어올려지지 않는다. 클래스를 선언하기 전에 인스턴스를 만들 수는 없다.
정적 메서드
클래스 바디의 메서드 선언 앞에 static 키워드를 붙여서 정적 메서드를 정의할 수 있다.
정적 메서드는 프로토타입 객체의 프로퍼티가 아니라 생성자 함수의 프로퍼티로 정의된다.
class Range {
constructor(from, to) {
this.from = from;
this.to = to;
}
includes(x) { return this.from <= x && x <= this.to; }
*[Symbol.iterator] () {
for(let x = Math.ceil(this.from); x <= this.to; x++) yield x;
}
toString() { return `(${this.from} ~ ${this.to})`; }
static parse(s) {
let matches = s.match(/^\((\d+) \~ (\d+)\)$/);
if (!matches) {
throw new TypeError(`Cannot parse Range from "${s}".`)
}
return new Range(parseInt(matches[1]), parseInt(matches[2]));
}
}
parse 메서드는 Range.prototype.parse()가 아니라 Range.parse()다.
인스턴스를 통해서는 호출할 수 없고 반드시 생성자를 통해 호출해야 한다.
let fromAtoB = Range.parse('(1 ~ 3)'); // 새 Range 객체 반환
fromAtoB.parse('(1 ~ 3)'); // TypeError: fromAtoB.parse는 함수가 아닙니다.
복소수 클래스
// Complex 클래스의 인스턴스는 복소수를 표현한다.
// 복소수는 실수와 허수의 합이며 허수 i는 -1의 제곱근이다.
class Complex {
// 클래스 필드 선언이 표준화되면
// 비공개 필드에서 복소수의 실수 부분과 허수 부분을 다음과 같이 쓸 수 있다.
// #r = 0;
// #i = 0;
// 이 생성자 함수는 모든 인스턴스에서 인스턴스 필드 r과 i를 정의한다.
// 이 필드는 각각 복소수의 실수 부분과 허수 부분을 나타내며 이들이 곧 객체의 상태다.
constructor(real, imaginary) {
this.r = real; // 실수 부분 필드
this.i = imaginary; // 허수 부분 필드
}
// 복소수의 덧셈과 곱셈을 담당하는 인스턴스 메서드
// c와 d가 이 클래스의 인스턴스라면 c.plus(d), d.times(c)처럼 쓸 수 있다.
plus(that) {
return new Complex(this.r + that.r, this.i + that.i);
}
times(that) {
return new Complex(this.r * that.r - this.i * that.i,
this.r * that.i + this.i * that.r);
}
// 복소수 연산 메서드의 정적 버전
// Complex.sum(c, d), Complex.product(c, d)처럼 쓸 수 있다.
static sum(c, d) { return c.plus(d); }
static product(c, d) { return c.times(d); }
// 필드처럼 쓸 수 있도록 게터로 정의한 인스턴스 메서드
// this.#r과 this.#i 비공개 필드를 사용했다면
// real과 imaginary 게터도 사용할 수 있다.
get real() { return this.r; }
get imaginary() { return this.i; }
get magnitude() { return Math.hypot(this.r, this.i); }
// 클래스에는 거의 항상 toString() 메서드가 있어야 한다.
toString() { return `{${this.r}, ${this.i}}`; }
// 클래스와 두 인스턴스가 같은 값을 나타내는지 확인할 수 있으면 좋다
equals(that) {
return that instanceof Complex &&
this.r === that.r &&
this.i === that.i;
}
// 클래스 바디 안에서 정적 필드가 지원된다면
// static ZERO = new Complex(0, 0);
// 같은 코드로 유용한 Complex.ZERO 상수를 정의할 수 있다.
// Complex.ONE = new Complex(1, 0);
// Complex.I = new Complex(0, 1);
// 또한 유용하다.
}
let c = new Complex(2, 3); // Complex {r: 2, i: 3}
let d = new Complex(c.i, c.r); // c의 인스턴스 필드를 사용. => Complex {r: 3, i: 2}
c.plus(d).toString() // => "{5, 5}"; 인스턴스 메서드 사용
c.magnitude // => Math.hypot(2, 3); 게터 함수 사용
Complex.product(c, d) // => new Complex(0, 13); 정적 메서드
Complex.ZERO.toString() // => "{0, 0}"; 정적 프로퍼티
9.4 기존 클래스에 메서드 추가
Complex 클래스에 켤레 복소수(complex conjugate)를 계산하는 메서드 추가
Complex.prototype.conj = function () { return new Complex(this.r, -this.i); };
9.5 서브클래스
객체 지향 프로그래밍에서 클래스 B가 다른 클래스 A를 확장(extend)할 때 A는 슈퍼클래스, B는 서브클래스라고 부른다.
B의 인스턴스느 A의 메서드를 상속한다. 클래스 B는 자신만의 메서드를 정의할 수 있고, 이 중 일부는 클래스 A에 있는 같은 이름의 메서드를 덮어 쓸 수 있다.
class Range {
constructor(from, to) {
this.from = from;
this.to = to;
}
includes(x) { return this.from <= x && x <= this.to; }
*[Symbol.iterator] () {
for(let x = Math.ceil(this.from); x <= this.to; x++) yield x;
}
toString() { return `(${this.from} ~ ${this.to})`; }
}
Range 클래스의 서브클래스인 Span을 만든다고 하자. 이 서브클래스는 일반적인 Range처럼 동작하지만, from과 to로 초기화하지 않고 start와 span으로 초기화한다. Span 클래스의 인스턴스는 Range 슈퍼클래스의 인스턴스이기도 하다. Span 인스턴스는 Span.prototype에서 커스터마이징한 toString() 메서드를 상속하지만, Range의 서브클래스이기도 하므로 Range.prototype에서 includes() 같은 메서드도 상속한다.
// 서브클래스에서 사용할 생성자 함수
function Span(start, span) {
if (span >= 0) {
this.from = start;
this.to = start + span;
} else {
this.to = start;
this.from = start + span;
}
}
// Span 프로토타입은 Range 프로토타입을 상속
Span.prototype = Object.create(Range.prototype);
// Range.prototype.constructor는 상속하지 않으므로 생성자 프로퍼티는 따로 정의
Span.prototype.constructor = Span;
// Span은 toString() 메서드를 따로 정의하므로 Range의 toString()을 상속하지 않고 덮어 쓴다.
Span.prototype.toString() = function() {
return `(${this.from} ~ ${this.to - this.from})`
};
아래가 핵심 코드다.
Span.prototype = Object.create(Range.prototype);
Span() 생성자로 생성된 객체는 Span.prototype 객체를 상속한다. Span.prototype은 Range.prototype을 상속하므로 Span 객체는 Span.prototype과 Range.prototype을 모두 상속한다.
extends와 super
// 키와 값 타입을 체크하는, Map의 서브클래스
class TypeMap extends Map {
constructor(keyType, valueType, entries) {
// entries가 지정됐으면 타입 체크
if (entries) {
for(let [k, v] of entries) {
if (typeof k !== keyType || typeof v !== valueType) {
throw new TypeError(`Wrong type for entry [${k}, ${v}]`);
}
}
}
// 타입을 체크한 entries로 슈퍼클래스를 초기화
super(entries);
// 타입을 저장하면서 서브클래스 초기화
this.keyType = keyType;
this.valueType = valueType;
}
// 맵에 추가되는 새 항목마다 타입을 체크하도록 set() 메서드 재정의
set(key, value) {
// 키나 값이 지정된 타입이 아닐 때 에러 발생
if (this.keyType && typeof key !== this.keyType) {
throw new TypeError(`${key} is not of type ${this.keyType}`);
}
if (this.valueType && typeof value !== this.valueType) {
throw new TypeError(`${value} is not of type ${this.valueType}`);
}
// 타입이 정확하면 슈퍼클래스의 set() 메서드를 호출해서 맵에 항목 추가
// 슈퍼클래스 메서드가 반환하는 것을 그대로 반환
return super.set(key, value);
}
}
- extends 키워드로 클래스를 정의하면 클래스 생성자는 슈퍼클래스 생성자를 호출할 떄 반드시 super()를 사용해야 한다.
- 서브클래스에 생성자를 정의하지 않으면 자동으로 생성된다. 이렇게 묵시적으로 정의된 생성자는 전달된 값을 그대로 super()에 전달한다.
- super()를 써서 슈퍼클래스 생성자를 호출하기 전에는 생성자 안에서 this 키워드를 사용하면 안 된다. 이 규칙을 따르면 서브클래스보다 슈퍼클래스를 먼저 초기화해야 한다는 규칙도 지킬 수 있다.
- 서브 클래스의 set() 메서드는 맵에 키와 값을 추가하는 방법을 모르지만 그 작업은 슈퍼클래스의 set() 메서드가 할 수 있으므로 super 키워드를 사용해 슈퍼클래스의 메서드를 호출한다. 이 컨텍스트에서 super는 this 키워드처럼 슈퍼클래스를 참조하므로 슈퍼클래스에 정의된 메서드에 접근할 수 있다.
'부트캠프 > 자바스크립트 완벽 가이드' 카테고리의 다른 글
10장. 모듈 (0) | 2023.02.13 |
---|---|
12장. 이터레이터와 제너레이터 (0) | 2023.02.12 |
8장. 함수 (0) | 2023.01.31 |
7장 배열 (0) | 2023.01.26 |
6장. 객체 (0) | 2023.01.15 |