25장 클래스
25.1 클래스는 프로토타입의 문법적 설탕인가?(25-01)
- 자바 스크립트는 클래스가 필요 없는 프로토타입 기반 객체지향 언어 ES5에서는 클래스처럼 생성자 함수와 프로토타입을 통해 상속을 구현
// ES5 생성자 함수
var Person = (function () {
// 생성자 함수
function Person(name) {
this.name = name;
}
// 프로토타입 메서드
Person.prototype.sayHi = function () {
console.log('Hi! My name is ' + this.name);
};
// 생성자 함수 반환
return Person;
}());
// 인스턴스 생성
var me = new Person('Lee');
me.sayHi(); // Hi! My name is Lee
25.2 클래스 정의(25-02)
// 클래스 선언문
class Person {}
25-03
- 표현식이란 것은 일급 객체라는 것을 의미
// 익명 클래스 표현식
const Person = class {};
// 기명 클래스 표현식
const Person = class MyClass {};
25-04
- 생성자, 프로토타입 메서드, 정적 메서드가 기존 방식과 차이가 있음
// 클래스 선언문
class Person {
// 생성자
constructor(name) {
// 인스턴스 생성 및 초기화
this.name = name; // name 프로퍼티는 public하다.
}
// 프로토타입 메서드
sayHi() {
console.log(`Hi! My name is ${this.name}`);
}
// 정적 메서드
static sayHello() {
console.log('Hello!');
}
}
// 인스턴스 생성
const me = new Person('Lee');
// 인스턴스의 프로퍼티 참조
console.log(me.name); // Lee
// 프로토타입 메서드 호출
me.sayHi(); // Hi! My name is Lee
// 정적 메서드 호출
Person.sayHello(); // Hello!
25.3 클래스 호이스팅(25-05)
- 클래스는 함수로 평가
// 클래스 선언문
class Person {}
console.log(typeof Person); // function
25-06
- 클래스는 클래스 정의 이전에 참조할 수 없다
console.log(Person);
// ReferenceError: Cannot access 'Person' before initialization
// 클래스 선언문
class Person {}
25-07
- 클래스도 호이스팅이 발생한다(TDZ에 클래스가 간다)
const Person = '';
{
// 호이스팅이 발생하지 않는다면 ''이 출력되어야 한다.
console.log(Person);
// ReferenceError: Cannot access 'Person' before initialization
// 클래스 선언문
class Person {}
}
25.4 인스턴스 생성(25-08)
- 클래스는 생성자 함수(new 연산자)
class Person {}
// 인스턴스 생성
const me = new Person();
console.log(me); // Person {}
25-09
- 일반 함수 호출
class Person {}
// 클래스를 new 연산자 없이 호출하면 타입 에러가 발생한다.
const me = Person();
// TypeError: Class constructor Foo cannot be invoked without 'new'
25-10
- 클래스 표현식
const Person = class MyClass {};
// 함수 표현식과 마찬가지로 클래스를 가리키는 식별자로 인스턴스를 생성해야 한다.
const me = new Person();
// 클래스 이름 MyClass는 함수와 동일하게 클래스 몸체 내부에서만 유효한 식별자다.
console.log(MyClass); // ReferenceError: MyClass is not defined
const you = new MyClass(); // ReferenceError: MyClass is not defined
25.5 메서드
25.5.1 constructor(25-11)
- constructor는 인스턴스 생성하고 초기화 위한 특수한 메서드
class Person {
// 생성자
constructor(name) {
// 인스턴스 생성 및 초기화
this.name = name;
}
}
25-12
- 크롬의 개발자 도구로 아래코드 실행시 클래스 내부 볼 수 있음
// 클래스는 함수다.
console.log(typeof Person); // function
console.dir(Person);
25-13
- 크롬의 개발자 도구로 아래코드 실행시 인스턴스 내부 볼 수 있음
// 인스턴스 생성
const me = new Person('Lee');
console.log(me);
25-14
// 클래스
class Person {
// 생성자
constructor(name) {
// 인스턴스 생성 및 초기화
this.name = name;
}
}
// 생성자 함수
function Person(name) {
// 인스턴스 생성 및 초기화
this.name = name;
}
25-15
-
클래스의 constructor메서드, 프로토타입의 constructor프로퍼티는 동일한게 아님. 프로토타입의 constructor프로퍼티는 모든 프로토타입이 가지며, 생성자 함수를 가리킴
-
클래스의 constructor메서드는 메서드로 해석이 되는게 아니라 클래스로 생성한 함수 객체 코드의 일부가 됨
-
클래스의 constructor메서드는 최대 한 개만 존재
class Person {
constructor() {}
constructor() {}
}
// SyntaxError: A class may only have one constructor
25-16
- 클래스의 constructor메서드는 생략 가능
class Person {}
25-17
- 클래스의 constructor메서드를 생략하면 암묵적으로 정의
class Person {
// constructor를 생략하면 다음과 같이 빈 constructor가 암묵적으로 정의된다.
constructor() {}
}
// 빈 객체가 생성된다.
const me = new Person();
console.log(me); // Person {}
25-18
- 프로퍼티가 추가된 인스턴스 생성하려면 constructor내부에 값 넣기
class Person {
constructor() {
// 고정값으로 인스턴스 초기화
this.name = 'Lee';
this.address = 'Seoul';
}
}
// 인스턴스 프로퍼티가 추가된다.
const me = new Person();
console.log(me); // Person {name: "Lee", address: "Seoul"}
25-19
- 외부 값을 적용하려면 constructor에 매개변수를 선언
class Person {
constructor(name, address) {
// 인수로 인스턴스 초기화
this.name = name;
this.address = address;
}
}
// 인수로 초기값을 전달한다. 초기값은 constructor에 전달된다.
const me = new Person('Lee', 'Seoul');
console.log(me); // Person {name: "Lee", address: "Seoul"}
25-20
- constructor는 별도의 반환문을 갖지 않아야 함(17.2.3절에서 이미 살펴본 내용)
class Person {
constructor(name) {
this.name = name;
// 명시적으로 객체를 반환하면 암묵적인 this 반환이 무시된다.
return {};
}
}
// constructor에서 명시적으로 반환한 빈 객체가 반환된다.
const me = new Person('Lee');
console.log(me); // {}
25-21
- 만약, 명시적으로 원시값 반환시 암묵적으로 this가 반환(이것도 이미 배운내용)
class Person {
constructor(name) {
this.name = name;
// 명시적으로 원시값을 반환하면 원시값 반환은 무시되고 암묵적으로 this가 반환된다.
return 100;
}
}
const me = new Person('Lee');
console.log(me); // Person { name: "Lee" }
25.5.2 프로토타입 메서드(25-22)
- 생성자 함수로 인스턴스 생성때의 프로토타입 메서드
// 생성자 함수
function Person(name) {
this.name = name;
}
// 프로토타입 메서드
Person.prototype.sayHi = function () {
console.log(`Hi! My name is ${this.name}`);
};
const me = new Person('Lee');
me.sayHi(); // Hi! My name is Lee
25-23
- 클래스의 경우 생략해도 기본적으로 프로토타입 메서드가 됨
class Person {
// 생성자
constructor(name) {
// 인스턴스 생성 및 초기화
this.name = name;
}
// 프로토타입 메서드
sayHi() {
console.log(`Hi! My name is ${this.name}`);
}
}
const me = new Person('Lee');
me.sayHi(); // Hi! My name is Lee
25-24
- 생성자 함수와 마찬가지로 클래스가 생성한 인스턴스는 프로토타입 체인의 일원이 됨
// me 객체의 프로토타입은 Person.prototype이다.
Object.getPrototypeOf(me) === Person.prototype; // -> true
me instanceof Person; // -> true
// Person.prototype의 프로토타입은 Object.prototype이다.
Object.getPrototypeOf(Person.prototype) === Object.prototype; // -> true
me instanceof Object; // -> true
// me 객체의 constructor는 Person 클래스다.
me.constructor === Person; // -> true
25.5.3 정적 메서드(25-25)
- 정적 메서드는 인스턴스를 생성하지 않아도 호출할 수 있는 걸 의미
// 생성자 함수
function Person(name) {
this.name = name;
}
// 정적 메서드
Person.sayHi = function () {
console.log('Hi!');
};
// 정적 메서드 호출
Person.sayHi(); // Hi!
25-26
- 클래스의 경우 앞에 static을 붙이면 됨
class Person {
// 생성자
constructor(name) {
// 인스턴스 생성 및 초기화
this.name = name;
}
// 정적 메서드
static sayHi() {
console.log('Hi!');
}
}
25-27
// 정적 메서드는 클래스로 호출한다.
// 정적 메서드는 인스턴스 없이도 호출할 수 있다.
Person.sayHi(); // Hi!
25-28
- 정적 메서드는 인스턴스로 호출 불가(상속 못 받기 때문)
// 인스턴스 생성
const me = new Person('Lee');
me.sayHi(); // TypeError: me.sayHi is not a function
25.5.4 정적 메서드와 프로토타입 메서드의 차이(25-29)
class Square {
// 정적 메서드
static area(width, height) {
return width * height;
}
}
console.log(Square.area(10, 10)); // 100
25-30
class Square {
constructor(width, height) {
this.width = width;
this.height = height;
}
// 프로토타입 메서드
area() {
return this.width * this.height;
}
}
const square = new Square(10, 10);
console.log(square.area()); // 100
25-31
// 표준 빌트인 객체의 정적 메서드
Math.max(1, 2, 3); // -> 3
Number.isNaN(NaN); // -> true
JSON.stringify({ a: 1 }); // -> "{"a":1}"
Object.is({}, {}); // -> false
Reflect.has({ a: 1 }, 'a'); // -> true
25.6 클래스의 인스턴스 생성 과정(25-32)
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가 암묵적으로 반환된다.
}
}
25.7 프로퍼티
25.7.1 인스턴스 프로퍼티(25-33)
- 인스턴스 프로퍼티는 constructor 내부에서 정의
class Person {
constructor(name) {
// 인스턴스 프로퍼티
this.name = name;
}
}
const me = new Person('Lee');
console.log(me); // Person {name: "Lee"}
25-34
class Person {
constructor(name) {
// 인스턴스 프로퍼티
this.name = name; // name 프로퍼티는 public하다.
}
}
const me = new Person('Lee');
// name은 public하다.
console.log(me.name); // Lee
25.7.2 접근자 프로퍼티(25-35)
- 접근자 함수로 구성된 프로퍼티다
const person = {
// 데이터 프로퍼티
firstName: 'Ungmo',
lastName: 'Lee',
// fullName은 접근자 함수로 구성된 접근자 프로퍼티다.
// getter 함수
get fullName() {
return `${this.firstName} ${this.lastName}`;
},
// setter 함수
set fullName(name) {
// 배열 디스트럭처링 할당: "36.1. 배열 디스트럭처링 할당" 참고
[this.firstName, this.lastName] = name.split(' ');
}
};
// 데이터 프로퍼티를 통한 프로퍼티 값의 참조.
console.log(`${person.firstName} ${person.lastName}`); // Ungmo Lee
// 접근자 프로퍼티를 통한 프로퍼티 값의 저장
// 접근자 프로퍼티 fullName에 값을 저장하면 setter 함수가 호출된다.
person.fullName = 'Heegun Lee';
console.log(person); // {firstName: "Heegun", lastName: "Lee"}
// 접근자 프로퍼티를 통한 프로퍼티 값의 참조
// 접근자 프로퍼티 fullName에 접근하면 getter 함수가 호출된다.
console.log(person.fullName); // Heegun Lee
// fullName은 접근자 프로퍼티다.
// 접근자 프로퍼티는 get, set, enumerable, configurable 프로퍼티 어트리뷰트를 갖는다.
console.log(Object.getOwnPropertyDescriptor(person, 'fullName'));
// {get: ƒ, set: ƒ, enumerable: true, configurable: true}
25-36
- 클래스에서도 사용 가능
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}
25-37
- 클래스의 접근자 프로퍼티 또한 인스턴스 프로퍼티가 아닌 프로토타입의 프로퍼티가 됨
// Object.getOwnPropertyNames는 비열거형(non-enumerable)을 포함한 모든 프로퍼티의 이름을 반환한다.(상속 제외)
Object.getOwnPropertyNames(me); // -> ["firstName", "lastName"]
Object.getOwnPropertyNames(Object.getPrototypeOf(me)); // -> ["constructor", "fullName"]
25.7.3 클래스 필드 정의 제안(25-38)
- 클래스 필드(또는 멤버)는 마치 내부에서 변수처럼 사용
// 자바의 클래스 정의
public class Person {
// ① 클래스 필드 정의
// 클래스 필드는 클래스 몸체에 this 없이 선언해야 한다.
private String firstName = "";
private String lastName = "";
// 생성자
Person(String firstName, String lastName) {
// ③ this는 언제나 클래스가 생성할 인스턴스를 가리킨다.
this.firstName = firstName;
this.lastName = lastName;
}
public String getFullName() {
// ② 클래스 필드 참조
// this 없이도 클래스 필드를 참조할 수 있다.
return firstName + " " + lastName;
}
}
25-39
- 자바 스크립트에선 클래스 몸체에 메서드만 선언 가능. 그러나 아래 예는 에러가 발생X
class Person {
// 클래스 필드 정의
name = 'Lee';
}
const me = new Person('Lee');
25-40
- 이유는 최신 브라우저나 Node에선 가능하게 만들어 놨기 때문
class Person {
// 클래스 필드 정의
name = 'Lee';
}
const me = new Person();
console.log(me); // Person {name: "Lee"}
25-41
- this는 클래스의 constructor와 메서드 내에서만 유효
class Person {
// this에 클래스 필드를 바인딩해서는 안된다.
this.name = ''; // SyntaxError: Unexpected token '.'
}
25-42
- 자바 같은 경우 클래스 필드 참조시 this생략 가능하지만 자바 스크립트는 아님
class Person {
// 클래스 필드
name = 'Lee';
constructor() {
console.log(name); // ReferenceError: name is not defined
}
}
new Person();
25-43
class Person {
// 클래스 필드를 초기화하지 않으면 undefined를 갖는다.
name;
}
const me = new Person();
console.log(me); // Person {name: undefined}
25-44
-
클래스 필드 초기화는 constructor에서 해야함
-
그러나, 굳이 이렇게 할 필요가 없는게 매번 클래스 필드를 선언해주고, 초기화를 해야하며 필드가 없어도 자동 추가 동작 한다
class Person {
name;
constructor(name) {
// 클래스 필드 초기화.
this.name = name;
}
}
const me = new Person('Lee');
console.log(me); // Person {name: "Lee"}
25-45
- name을 클래스 필드로 선언 안했어도 자동으로 추가된다
class Person {
constructor(name) {
this.name = name;
}
}
const me = new Person('Lee');
console.log(me); // Person {name: "Lee"}
25-46
- 클래스 필드에 함수도 할당 가능
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
25-47
- 모든 클래스 필드는 인스턴스 프로퍼티가 됨
<!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>
25.7.4 private 필드 정의 제안(25-48)
class Person {
constructor(name) {
this.name = name; // 인스턴스 프로퍼티는 기본적으로 public하다.
}
}
// 인스턴스 생성
const me = new Person('Lee');
console.log(me.name); // Lee
25-49
class Person {
name = 'Lee'; // 클래스 필드도 기본적으로 public하다.
}
// 인스턴스 생성
const me = new Person();
console.log(me.name); // Lee
25-50
- 이것도 최신에 나온 기법(2021년 기준)
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
25-51
- private는 클래스 내부에서만 접근 가능하지만, 접근자 프로퍼티를 토앻 간접적으로 접근도 가능
class Person {
// private 필드 정의
#name = '';
constructor(name) {
this.#name = name;
}
// name은 접근자 프로퍼티다.
get name() {
// private 필드를 참조하여 trim한 다음 반환한다.
return this.#name.trim();
}
}
const me = new Person(' Lee ');
console.log(me.name); // Lee
25-52
- private 필드는 클래스 몸체에서만 정의
class Person {
constructor(name) {
// private 필드는 클래스 몸체에서 정의해야 한다.
this.#name = name;
// SyntaxError: Private field '###name' must be declared in an enclosing class
}
}
25.7.5 static 필드 정의 제안(25-53)
- static 필드도 최근 만들어 짐(2021년 기준)
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
25.8 상속에 의한 클래스 확장
25.8.1 클래스 상속과 생성자 함수 상속(25-54)
-
지금까지 본 프로토타입 기반 상속과 클래스 기반 상속은 다른 개념
-
상속에 의한 클래스 확장은 기존 클래스를 상속받아 새로운 클래스를 확장(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
25-55
- 클래스 상속 새로 구현되기 전에 사용하던 방식(참고만 하라)
// 의사 클래스 상속(pseudo classical inheritance) 패턴
var Animal = (function () {
function Animal(age, weight) {
this.age = age;
this.weight = weight;
}
Animal.prototype.eat = function () {
return 'eat';
};
Animal.prototype.move = function () {
return 'move';
};
return Animal;
}());
// Animal 생성자 함수를 상속하여 확장한 Bird 생성자 함수
var Bird = (function () {
function Bird() {
// Animal 생성자 함수에게 this와 인수를 전달하면서 호출
Animal.apply(this, arguments);
}
// Bird.prototype을 Animal.prototype을 프로토타입으로 갖는 객체로 교체
Bird.prototype = Object.create(Animal.prototype);
// Bird.prototype.constructor을 Animal에서 Bird로 교체
Bird.prototype.constructor = Bird;
Bird.prototype.fly = function () {
return 'fly';
};
return Bird;
}());
var bird = new Bird(1, 5);
console.log(bird); // Bird {age: 1, weight: 5}
console.log(bird.eat()); // eat
console.log(bird.move()); // move
console.log(bird.fly()); // fly
25.8.2 extends 키워드(25-56)
-
extends 키워드의 역할은 수퍼, 서브 클래스의 상속을 설정 클래스의 프로토타입 체인
-
클래스도 프로토타입을 통해 상속 관계를 구현 인스턴스의 프로토타입 체인
-
이를 통해 프로토타입 메서드, 정적 메서드 모두 상속이 가능
// 수퍼(베이스/부모)클래스
class Base {}
// 서브(파생/자식)클래스
class Derived extends Base {}
25.8.3 동적 상속(25-57)
// 생성자 함수
function Base(a) {
this.a = a;
}
// 생성자 함수를 상속받는 서브클래스
class Derived extends Base {}
const derived = new Derived(1);
console.log(derived); // Derived {a: 1}
25-58
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
25.8.4 서브클래스의 constructor(25-59)
- 클래스에 constructor생략하면 다음과 같이 빈 constructor객체 생성(암묵적)
constructor() {}
25-60
- 서브클래스에서 constructor생략하면 다음과 같이 객체 생성(args는 클래스를 호출할 때 전달한 인수의 리스트)
constructor(...args) { super(...args); }
25-61
- 아래는 둘다 constructor를 생략했다
// 수퍼클래스
class Base {}
// 서브클래스
class Derived extends Base {}
25-62
- constructor를 생략하면 이렇게 암무적으로 정의
// 수퍼클래스
class Base {
constructor() {}
}
// 서브클래스
class Derived extends Base {
constructor() { super(); }
}
const derived = new Derived();
console.log(derived); // Derived {}
25.8.5 super 키워드(25-63)
- super를 호출하면 수퍼클래스의 constructor(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}
25-64
- 서브클래스에서 수퍼클래스의 constructor에 전달해야하는 경우 super를 통해 전달
// 수퍼클래스
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}
25-65
-
super 호출
-
1.서브클래스에서 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();
25-66
- 2.서브클래스의 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);
25-67
- 3.super는 반드시 서브클래스의 constructor에서만 호출
class Base { // 그냥 클래스
constructor() {
super(); // SyntaxError: 'super' keyword unexpected here
}
}
function Foo() { // 그냥 함수
super(); // SyntaxError: 'super' keyword unexpected here
}
25-68
-
super 참조
-
1.서브클래스의 프로토타입 메서드 내에서 super.sayHi는 수퍼클래스의 프로토타입 메서드 sayHi를 가리킴
// 수퍼클래스
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?
25-69
// 수퍼클래스
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?`;
}
}
25-70
- super 참조를 의사 코드로 표현
/*
[[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]])
25-71
- 주의할 점은 ES6의 메서드 축약 표현인 함수만 [[HomeObject]] 가짐
const obj = {
// foo는 ES6의 메서드 축약 표현으로 정의한 메서드다. 따라서 [[HomeObject]]를 갖는다.
foo() {},
// bar는 ES6의 메서드 축약 표현으로 정의한 메서드가 아니라 일반 함수다.
// 따라서 [[HomeObject]]를 갖지 않는다.
bar: function () {}
};
25-72
- 메서드 축약 표현인 함수의 객체 리터럴에서도 super 참조 사용 가능
const base = {
name: 'Lee',
sayHi() {
return `Hi! ${this.name}`;
}
};
const derived = {
__proto__: base,
// ES6 메서드 축약 표현으로 정의한 메서드다. 따라서 [[HomeObject]]를 갖는다.
sayHi() {
return `${super.sayHi()}. how are you doing?`;
}
};
console.log(derived.sayHi()); // Hi! Lee. how are you doing?
25-73
- 서브클래스의 정적 메서드 내에서 super.sayHi는 수퍼클래스의 정적 메서드 sayHi를 가리킴
// 수퍼클래스
class Base {
static sayHi() {
return 'Hi!';
}
}
// 서브클래스
class Derived extends Base {
static sayHi() {
// super.sayHi는 수퍼클래스의 정적 메서드를 가리킨다.
return `${super.sayHi()} how are you doing?`;
}
}
console.log(Derived.sayHi()); // Hi! how are you doing?
25.8.6 상속 클래스의 인스턴스 생성 과정(25-74)
// 수퍼클래스
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
25-75
- 1.서브클래스의 super 호출 2.수퍼클래스의 인스턴스 생성과 this 바인딩
// 수퍼클래스
class Rectangle {
constructor(width, height) {
// 암묵적으로 빈 객체, 즉 인스턴스가 생성되고 this에 바인딩된다.
console.log(this); // ColorRectangle {}
// new 연산자와 함께 호출된 함수, 즉 new.target은 ColorRectangle이다.
console.log(new.target); // ColorRectangle
...
25-76
// 수퍼클래스
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
...
25-77
- 3.수퍼클래스의 인스턴스 초기화
// 수퍼클래스
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}
}
...
25-78
- 4.서브클래스 constructor로의 복귀와 this 바인딩 5.서브클래스의 인스턴스 초기화
// 서브클래스
class ColorRectangle extends Rectangle {
constructor(width, height, color) {
super(width, height);
// super가 반환한 인스턴스가 this에 바인딩된다.
console.log(this); // ColorRectangle {width: 2, height: 4}
...
25-79
- 6.인스턴스 반환
// 서브클래스
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"}
}
...
25.8.7 표준 빌트인 생성자 함수 확장(25-80)
- 표준 빌트인 생성자 함수를 상속받아 확장
// 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
25-81
- 타입은 Array가 아니라 MyArray(당연)
console.log(myArray.filter(v => v % 2) instanceof MyArray); // true
25-82
- 메서드 체이닝은 해당 코드처럼 .uniq().average()…이런식으로 연속으로 메서드 사용가능한 것(각각 반환하면서 넘어감)
// 메서드 체이닝
// [1, 1, 2, 3] => [ 1, 1, 3 ] => [ 1, 3 ] => 2
console.log(myArray.filter(v => v % 2).uniq().average()); // 2
25-83
- Symbol.species를 사용해 정적 접근자 프로퍼티를 추가시켜 Array로 반환 받을 수 있다
// Array 생성자 함수를 상속받아 확장한 MyArray
class MyArray extends Array {
// 모든 메서드가 Array 타입의 인스턴스를 반환하도록 한다.
static get [Symbol.species]() { return 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.uniq() instanceof MyArray); // false
console.log(myArray.uniq() instanceof Array); // true
// 메서드 체이닝
// uniq 메서드는 Array 인스턴스를 반환하므로 average 메서드를 호출할 수 없다.
console.log(myArray.uniq().average());
// TypeError: myArray.uniq(...).average is not a function
댓글남기기