목표
25. 클래스 - 상속(extends)
※ 본문의 목차는 책의 목차와 다릅니다.
25-8. 클래스 상속과 생성자 함수 상속
- 클래스 기반 상속은 기존 클래스를 상속받아 새로운 클래스를 확장(extends)하여 정의하는 것이다.
- 클래스와 생성자 함수는 인스턴스를 생성할 수 있는 함수라는 점에서 유사하지만 클래스는 상속을 통해 기존 클래스를 확장할 수 있는 문법이 제공되지만 생성자 함수는 제공하지 않는다.
class Animal {
constructor(age, weight) {
this.age = age;
this.weight = weight;
}
move() { return "move"; }
}
class Lion extends Animal {
hunt() { return "hunt"; }
}
const lion = new Lion(4, 21);
console.log(lion instanceof Lion);
console.log(lion instanceof Animal);
console.log(lion.move());
console.log(lion.hunt());
true
true
"move"
"hunt"
25-9. extends 키워드
class Animal {}
class Lion extends Animal {}
- 상속을 통해 클래스를 확장하려면 extends 키워드를 통해 정의한다.
- 상속을 통해 확장된 클래스를 서브클래스(위 코드에서는 Lion) 또는 자식클래스이라 부른다.
- 서브클래스에게 상속된 클래스를 수퍼클래스(위 코드에서는 Animal) 또는 부모클래스라고 부른다.
function Animal(age) {
this.age = age;
}
class Lion extends Animal {}
const lion = new Lion(7);
console.log(lion.age);
7
- 또한, extends 키워드를 사용해서 클래스 뿐만 아니라 생성자 함수도 상속받을 수 있다.
- 단, extends 키워드 앞에는 반드시 클래스가 와야 한다.
25-10. 동적 상속
function Animal() {}
class Plant {}
let flag = true;
class Lion extends (flag ? Animal : Plant) {}
flag = false;
class Tree extends (flag ? Animal : Plant) {}
const lion = new Lion();
const tree = new Tree();
console.log(lion instanceof Animal);
console.log(lion instanceof Plant);
console.log(tree instanceof Animal);
console.log(tree instanceof Plant);
true
false
false
true
- extends 키워드 다음에는 클래스 뿐만 아니라 삼항 연산자와 같은 표현식을 사용해 동적으로 상속받을 대상을 결정할 수 있다.
25-11. 서브클래스의 constructor
- 클래스는 constructor를 생략하면 constructor가 암묵적으로 정의된다.
class Animal {}
class Animal {
constructor() {}
}
- 마찬가지로 서브클래스도 constructor를 생략하면 암묵적으로 정의된다.
class Animal {}
class Monkey extends Animal {}
class Animal {
constructor() {}
}
class Monkey extends Animal {
constructor(...agrs) {
super(...args);
}
}
- 서브클래스는 수퍼클래스와 다르게 agrs라는 매개변수와 함께 정의된다.
- agrs 는 new 연산자와 함께 클래스를 호출할 때 전달한 인수의 리스트이다.
- super()는 수퍼클래스의 constructor()를 호출하여 인스턴스를 생성한다.
Rest 파라미터
- 매개변수에 ...을 붙이면 Rest 파라미터가 되는데 함수에 전달된 인수들의 목록을 배열로 전달받는다.
25-12. super 키워드
- super 키워드는 다음과 같은 역할을 한다.
- 함수처럼 호출해 수퍼클래스의 생성자(constructor)를 호출 한다.
- super를 참조하면 수퍼클래스의 메서드를 호출 할 수 있다.
25-12-1. super 호출
class Animal {
constructor(a, b) {
this.a = a;
this.b = b;
}
}
class Monkey extends Animal {
constructor(a, b, c) {
super(a, b);
this.c = c;
}
}
const monkey = new Monkey(1, 2, 3);
console.log(monkey);
Monkey {a: 1, b: 2, c: 3}
- new 연산자를 통해 클래스를 호출하면서 전달된 인수 1, 2, 3은 Monkey 클래스의 constructor에 전달된다.
- 그 후, super를 호출해 Animal 클래스의 constructor에 일부가 전달된다.
- 이처럼 인스턴스 초기화를 위해 전달된 인수는 수퍼클래스와 서브클래스에 배분되고 상속 관계의 두 클래스는 서로 협력하여 인스턴스를 생성하게 된다.
- 단, super를 호출할 때 주의할 사항이 있다.
- 서브클래스에서 constructor를 생략하지 않는 경우 즉, constructor를 정의할 경우에는 반드시 super를 호출해야 한다.
class Animal {}
class Rabbit extends Animal {
constructor() {}
}
const rabbit = new Rabbit();
"Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor"
- 서브클래스의 constructor에서 super를 호출하기 전까지 this를 참조할 수 없다.
class Animal {}
class Rabbit extends Animal {
constructor(a) {
this.a = a;
super();
}
}
const rabbit = new Rabbit(1);
"ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor"
- super는 반드시 서브클래스의 constructor에서만 호출해야 한다.
class Animal {
constructor() {
super();
}
}
class Rabbit extends Animal {
constructor(a) {
super();
}
}
const rabbit = new Rabbit(1);
"Uncaught SyntaxError: 'super' keyword unexpected here"
25-12-2. super 참조
- 메서드 내에서 super를 참조하면 수퍼클래스의 메서드를 호출할 수 있다.
- 서브클래스의 프로토타입 메서드 내에서 super를 사용해 수퍼클래스의 프로토타입 메서드를 참조할 수 있다.
class Parent {
constructor(job1, job2) {
this.mom = job1;
this.dad = job2;
}
getParentJob() {
return `mom is ${this.mom} and dad is ${this.dad}`
}
}
class Child extends Parent {
constructor(name, age1, age2) {
super(age1, age2);
this.name = name;
}
getParentJob() {
return `${this.name}'s ${super.getParentJob()}`
}
}
const james = new Child("james", "reporter", "programmer");
console.log(james.getParentJob());
"james's mom is reporter and dad is programmer"
- 서브클래스의 정적 메서드 내에서 super를 사용해 수퍼클래스의 정적 메서드를 참조할 수 있다.
class Animal {
static type() {
return "is Animal";
}
}
class Tiger extends Animal {
static type() {
return `tiger ${super.type()}`
}
}
console.log(Tiger.type());
"tiger is Animal"
25-13. 상속 클래스의 인스턴스 생성 과정
- 직사각형을 추상화한 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);
console.log(colorRectangle.getArea());
console.log(colorRectangle.toString());
ColorRectangle {width: 2, height: 4, color: 'red'}
8
"width = 2, height = 4, color = red"
- 서브클래스 ColorRectangle이 new 연산자와 함께 호출되면 다음 과정을 통해 인스턴스를 생성한다.
- 서브클래스의 super 호출
- 수퍼클래스의 인스턴스 생성과 this 바인딩
- 수퍼클래스의 인스턴스 초기화
- 서브클래스 constructor로의 복귀와 this 바인딩
- 서브클래스의 인스턴스 초기화
- 인스턴스 반환
25-13-1. 서브클래스의 super 호출
- 상속받지 않는 클래스 는 new 연산자와 함께 호출되었을 때 암묵적으로 빈 객체를 생성해 이를 this에 바인딩한다.
- 하지만, 서브클래스는 자신이 직접 인스턴스를 생성하지 않고 수퍼클래스에게 인스턴스 생성을 위임한다. 이 때문에 서브클래스의 constructor에서 반드시 super를 호출해야 한다. 호출하지 않으면 에러가 발생한다.
- super 키워드가 함수처럼 호출되면 수퍼클래스의 constructor가 호출된다.
25-13-2. 수퍼클래스의 인스턴스 생성과 this 바인딩
- 수퍼클래스의 constructor가 실행되기 전에 빈 객체를 생성하고 this에 바인딩된다.
- 수퍼클래스의 constructor 내부의 this는 생성된 인스턴스를 가리킨다. 즉, 인스턴스는 수퍼클래스가 생성했지만 new 연산자와 함께 호출된 서브클래스를 가리키게 된다.
- 프로토타입 관점에서 살펴보면 생성된 인스턴스의 프로토타입은 수퍼클래스의 prototype 프로퍼티가 가리키는 객체 Rectangle이 아니라 서브클래스의 prototype 프로퍼티가 가리키는 객체인 ColorRectangle이다.
class Rectangle {
console.log(this);
}
class ColorRectangle extends Rectangle {}
const colorRectangle = new ColorRectangle(2, 4, "red");
25-13-3. 수퍼클래스의 인스턴스 초기화
- 인스턴스를 생성하고 this에 바인딩 되었으면 이제 초기화 작업을 진행한다.
- this에 바인딩되어 있는 인스턴스에 프로퍼티를 추가하고 constructor가 인수로 전달받은 초기값으로 인스턴스의 프로퍼티를 초기화한다.
class Rectangle {
constructor(width, height) {
this.width = width;
this.height = height;
console.log(this);
}
}
class ColorRectangle extends Rectangle {}
const colorRectangle = new ColorRectangle(2, 4, "red");
25-13-4. 서브클래스 constructor로의 복귀와 this 바인딩
- 수퍼클래스의 초기화 작업이 완료되면 super 호출이 종료되고 제어 흐름이 서브 클래스 constructor로 다시 돌아온다.
- 이 때, super가 반환한 인스턴스가 this에 바인딩된다.
- 서브클래스는 별도의 인스턴스를 생성하지 않고 super가 반환한 인스턴스를 this에 바인딩하여 그대로 사용한다.
class ColorRectangle extends Rectangle {
constructor(width, hieght, color) {
console.log(this);
super(width, height);
console.log(this);
}
}
const colorRectangle = new ColorRectangle(2, 4, "red");
- 이처럼 super가 호출되지 않으면 인스턴스가 생성되지 않으며, this에 바인딩도 할 수 없다.
- 서브클래스의 constructor에서 suepr를 호출하기 전에는 this를 참조할 수 없는 이유가 바로 이 때문이다.
25-13-5. 서브클래스 인스턴스 초기화
- this에 바인딩되어 있는 인스턴스에 프로퍼티를 추가하고 constructor가 인수로 전달받은 초기값으로 프로퍼티를 초기화한다.
class ColorRectangle extends Rectangle {
constructor(width, hieght, color) {
super(width, height);
this.color = color;
}
}
25-13-6. 인스턴스 반환
- 완성된 인스턴스가 바인딩된 this가 암묵적으로 반환된다.
class ColorRectangle extends Rectangle {
constructor(width, hieght, color) {
super(width, height);
console.log(this);
this.color = color;
console.log(this);
}
}
const colorRectangle = new ColorRectangle(2, 4, "red");
25-14. 표준 빌트인 생성자 함수 확장
- extends 키워드로 String, Number, Array 같은 표준 빌트인 객체도 확장할 수 있다.
class MyArray extends Array {
uniq() {
return this.filter((v, i, self) => self.indexOf(v) === i);
}
average() {
return this.reduce((pre, cur) => pre + cur, 0) / this.length;
}
}
const myArray = new MyArray(1, 1, 2, 3);
console.log(myArray.uniq());
console.log(myArray.average());
MyArray(3) [1, 2, 3]
1.75
- Array 생성자 함수를 상속받아 확장한 MyArray 클래스가 생성한 인스턴스는 Array.prototype과 MyArray.prototype의 모든 메서드를 사용할 수 있다.
- 주의할 점은 Array.prototype의 메서드인 map이나 filter를 사용해도 Array 클래스가 아닌 MyArray 클래스 인스턴스가 반환된다.
console.log(myArray.filter(v => v % 2).uniq().average());
- filter를 사용해도 MyArray 클래스를 반환하기 때문에 uniq와 average 메서드를 연이어 호출할 수 있다.