# ES6 클래스

  • 생성자 함수를 더 깔끔한 문법으로 정의 가능
  • Syntatic Sugar prototype 기반 상속을 보다 명료하게 사용할 수 있는 문법을 제공
  • 다른언어의 Class 문법과 생김새는 비슷함
  • BUT 내부 동작은 다름 : prototype 기반으로 상속을 흉내냄
  • 새로운 블록 스코프를 형성

Syntatic Sugar: 읽고 표현하는 것을 더 쉽게 하기 위해서 고안된 프로그래밍 언어 문법

# Class Syntax

예시

class Person {
    // 생성자 함수는 constructor 으로 정의함.
    constructor({ name, age }) {
        this.name = name;
        this.age = age;
    }
    // 객체의 메소드 정의 문법과 같다.
    // 객체의 메서드는 Peron.prototype 에 저장된다.
    introduce () {
        return `안녕하세요, 제 이름은 ${this.name} 입니다.`;
    }
}
const person = new Person({ name: '박은영', age: 19 });
console.log(person.introduce());
1
2
3
4
5
6
7
8
9
10
11
12
13
14

사용할 수 없는 문법

class Person {
    console.log('hello');   // Unexpected token
    prop: 1,    // Unexpected token
}
1
2
3
4

# Class 정의

JS 의 Class 는 함수. 따라서, class 정의 문법

  • Class 선언식

  • Class 표현식

  • class 키워드

    • 클래스를 선언.
    • JS 엔진이 class 키워드를 만나면 Class Object 를 생성.
  • Class Object

    • JS 엔진이 class 키워드로 생성한 객체.

# Class 선언

함수 선언과 달리 클래스 선언은 uninitialized 로 초기화.

  • 따라서 클래스 사용전에 미리 선언을 해야 한다.

왜 그런 차이가 발생하게 되는 것일까요? TODO

class People { 
    constructor(name) {
        this.name= name;
    }
}
1
2
3
4
5

# Class 표현식

# 명명된 클래스 표현식

const People = class People {
    // ...
}
1
2
3

# 익명 클래스 표현식

const People = class {
    // ...
}
1
2
3

# 일급 객체 Class

함수처럼 다른 표현식 내부에서 전달, 반환, 할당 가능

function makeClass(phrase) {
  // 클래스를 선언하고 이를 반환함
  return class {
    sayHi() {
      alert(phrase);
    };
  };
}
let User = makeClass("Hello");
new User().sayHi(); // Hello
1
2
3
4
5
6
7
8
9
10

조건에 따라 다른 클래스를 상속받고 싶을 때,

function f(phrase) {
  return class {
    sayHi() { alert(phrase) }
  }
}

class User extends f("Hello") {}

new User().sayHi(); // Hello
1
2
3
4
5
6
7
8
9

# 클래스 인스턴스

new 연산자와 함께 생성자(constructor) 호출함.

class Foo {}
const foo = new Foo();

console.log(Foo === Foo.prototype.constructor)
1
2
3
4

constructor 은 new 연산자 없이 호출할 수 없다.

const foo = Foo();  // TypeError
1

# constructor

  • 클래스의 인스턴스 생성 & 인스턴스 프로퍼티 초기화
  • 인스턴스 프로퍼티의 동적 할당 및 초기화
    class Foo {}
    const foo = new Foo();
    foo.num = 1;
    
    1
    2
    3

class User 선언 결과 class User 선언 결과

# 클래스 프로퍼티

클래스 body 에는 메서드만 선언 가능.

class Foo {
    name = "";  // SyntaxError
}
1
2
3

이 문법은 현재 클래스 필드 로 바벨없이 대부분의 브라우저에서 사용가능하다.

클래스 프로퍼티의 선언과 초기화는 constructor 내부에서 해야 한다.

class Foo {
    constructor(name = "") {
        this.name = name;
    }
}
1
2
3
4
5

this

  • 클래스 인스턴스를 가리킨다.

# 클래스 메서드

객체 리터럴에서 사용하던 문법과 유사하다.

const methodName = 'introduce';
class Calculator {
    add(x, y) {
        return x + y;
    }
    subtract(x, y) {
        return x - y;
    }
    [methodName] () {   // computed method name   
        return `hello`;
    }
}
console.log(new Calculator().introduce());  // hello
1
2
3
4
5
6
7
8
9
10
11
12
13

# Getter, Setter 정의

computed property

class Account {
    constructor() {
        this._balance = 0;
    }
    get balance() {
        return this._balance;
    }
    set balance(newBalance) {
        this._balance = newBalance;
    }
}
const account = new Account();
account.balance = 10000;
account.balance; // 10000
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 클래스 Generator 메서드

class Gen {
    *[Symbol.iterator]() {
        yield 1;
        yield 2;
        yield 3;
    }
}
for (let n of new Gen()) {
    console.log(n);
}   // 1, 2, 3
1
2
3
4
5
6
7
8
9
10

# 클래스 static 메서드

객체가 아닌 클래스 함수에 속한 메서드.

  • 일반 메서드는 프로토타입 객체의 메서드이다.
  • static method 는 생성자 함수의 메서드 이다.

  • 개별 인스턴스에 묶이지 않는다.

  • 클래스의 static 인스턴스에만 접근할 수 있다.

  • Math 객체의 메소드처럼, 애플리케이션 전역에서 사용할 유틸리티(utility) 함수를 생성할 때 주로 사용한다. 프로토타입과 정적 메서드

  • 정적 메서드 내에서의 this

class Foo {}
Foo.a = 10;
Foo.b = 20;
Foo.c = function () {
  return this.a + this.b;
};
console.log(Foo.c()); // 30
1
2
3
4
5
6
7

예시1) 인스턴스 끼리의 연산

class Person {
    constructor({ name, age }) {
        this.name = name;
        this.age = age;
    }
    static sumAge(...people) {
        return people.reduce((acc, person) => acc + person.age, 0);
    }
}
const person1 = new Person({ name: "윤아준", age: 19 });
const person2 = new Person({ name: "신하경", age: 20 });

Person.sumAge(person1, person2); // 39
1
2
3
4
5
6
7
8
9
10
11
12
13
Person.compareAge = function(person1, person2) {
    // ...
}
1
2
3

예시2) 인스턴스 끼리 비교

class Article {
  constructor(title, date) {
    this.title = title;
    this.date = date;
  }

  static compare(articleA, articleB) {
    return articleA.date - articleB.date;
  }
}

// 사용법
let articles = [
  new Article("HTML", new Date(2019, 1, 1)),
  new Article("CSS", new Date(2019, 0, 1)),
  new Article("JavaScript", new Date(2019, 11, 1))
];

articles.sort(Article.compare);

alert( articles[0].title ); // CSS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

예시 3) ‘팩토리’ 메서드를 구현한 코드입니다. 다양한 방법을 사용해 조건에 맞는 article 인스턴스를 만들어야 한다고 가정해 봅시다.

  1. 매개변수(title, date 등)를 이용해 관련 정보가 담긴 article 생성
  2. 오늘 날짜를 기반으로 비어있는 article 생성
  3. 기타 등등
  • 생성자 방법 & 클래스에 정적 메서드를 만들어 구현
class Article {
  constructor(title, date) {
    this.title = title;
    this.date = date;
  }

  static createTodays() {
    // this는 Article입니다.
    return new this("Today's digest", new Date());
  }
}

let article = Article.createTodays();

alert( article.title ); // Today's digest

// Article은 article을 관리해주는 특별 클래스라고 가정합시다.
// article 삭제에 쓰이는 정적 메서드
Article.remove({id: 12345});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 클래스 필드

클래스 내부의 캡슐화된 변수

  • 데이터 멤버, 멤버 변수
  • 인스턴스의 (프로퍼티 & 정적 프로퍼티)가 될 수 있다.
  • 자바스크립트의 생성자 함수에서 this 에 추가한 프로퍼티를 클래스 기반 객체지향 언어에서는 클래스 필드라고 부른다.

클래스 블록 안에서 할당연산자를 이용해 인스턴스 속성을 지정할 수 있는 문법

class Counter {
    static initial = 0;         // static class field
    count = Counter.initial;    // class field
    inc() {
        return this.count++;
    }
}
const counter = new Counter();
alert(counter.count);           // 0
alert(counter.prototype.name)   // undefined
1
2
3
4
5
6
7
8
9
10

# 클래스 내의 this

인스턴스 객체를 가리킨다.

class MyClass {
    a = 1;
    b = this.a;
}
new MyClass().b; //1
1
2
3
4
5

# 화살표 함수를 통해 메서드 정의

class MyClass {
    a = 1;
    getA = () => {
        return this.a;
    }      
}
new MyClass().getA();
1
2
3
4
5
6
7

# 일반 클래스 메서드

prototype 속성에 저장

# 클래스 필드(화살표함수)

인스턴스 객체에 저장

  • 화살표 함수의 this 는 항상 인스턴스 객체를 가리킴.
  • 인스턴스를 생성할 때마다 새로 생성되기 때문에 메모리를 더 차지하게 된다.
  • 메서드를 값으로 다루어야 할 경우 (TODO)
  • 메서드를 다른 함수로 넘겨 줘야 되는 경우 (TODO)
  • 브라우저 환경에서 메서드를 이벤트 리스너로 설정해야 할 때.
    객체 메서드를 전달해 전혀 다른 컨텍스트에서 호출하게 되면 this 는 원래 객체를 참조하지 않습니다.
    class Button {
      constructor(value) {
        this.value = value;
      }
    
      click() {
        alert(this.value);
      }
    }
    
    let button = new Button("hello");
    
    setTimeout(button.click, 1000); // undefined
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    이때 click 을 화살표 함수로 만들면 Button 객체마다 독립적인 함수를 만들고 함수의 this 를 해당 객체에 바인딩 해준다.
    // ...
    click = () => {
      alert(this.value);
    };
    // ...
    setTimeout(button.click, 1000); // hello
    
    1
    2
    3
    4
    5
    6

# private, static, public 필드

class Foo {
    x = 1;              // Field declaration
    #p = 0;             // Private field
    static y = 20;      // Static public field
    static #sp = 30;    // Static private field

    static #sm() {      // Static private method
        console.log('static private method');
    }
}
1
2
3
4
5
6
7
8
9
10

# 클래스 상속

단일 상속

# 재사용

클래스의 기능을 다른 클래스에서 재사용

class Person { 
    static staticProp = "staticProp";
      static staticMethod() {
        return "I'm a static method.";
      }
      instanceProp = "instanceProp";
      instanceMethod() {
        return "I'm a instance method.";
      }
}
class Student extends Person {
    // ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
  • Student 클래스는 Person 클래스 의 static 메서드, 필드을 사용할 수 있다.
    Student.staticProp;
    Student.staticMethod();
    
    1
    2
  • Student 클래스 인스턴스는 Person 클래스의 메서드와, 필드 을 사용할 수 있다.
    const s = new Student();
    s.instanceProp;
    s.instanceMethod();
    
    1
    2
    3

# super

서브 클래스에서 수퍼 클래스와 같은 이름의 속성을 정의하면 수퍼 클래스의 속성이 가려진다.
이 때, 수퍼 클래스의 속성에 접근하고 싶을 때, super 키워드를 사용하면 된다.

class Melon  {
    getColor() {
        return "제 색깔은 초록색 입니다.";   
    }
} 
class WaterMelon extends Melon {
    getColor() {
        return super.getColor() + 'but 속은 빨강색 입니다.'
    }
}
1
2
3
4
5
6
7
8
9
10

# super 키워드 동작방식

  1. 서브 클래스 생성자에서 super()
    • 수퍼 클래스의 생성자 호출
    • 서브 클래스의 생성자에서 super 를 호출하지 않으면 this 에 대한 참조에러가 발생함.
  2. 서브 클래스 static 메서드에서 super.속성명
    • 수퍼 클래스의 static 속성 접근
  3. 서브 클래스 인스턴스 메서드에서 this.속성명
    • 수퍼 클래스의 인스턴스 속성에 접근

댓글을 참고해주세요

class Person {
    constructor({ name, age }) {
        this.name = name;
        this.age = age;
    }  
    introduce() {
        return `제 이름은 ${this.name}입니다.`;
    }
}
class Student extends Person {
    constructor({ grade, ...rest}) {
        super(rest);
        this.grade = grade;
    }
    introduce(){
        return super.introduce() + ` 저는 ${this.grade}학년입니다.`;
    }   
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 화살표 함수 super

화살표 함수는 super 을 쓸 수 없다.

화살표 함수를 써야하는 '브라우저 환경에서 메서드를 이벤트 리스너로 설정해야 할 때' 는 아래와 같이 해준다.

class Rabbit extends Animal {
  stop() {
    setTimeout(() => super.stop(), 1000); // 1초 후에 부모 stop을 호출합니다.
  }
}
1
2
3
4
5

# 생성자 오버라이딩

자체 생성자가 없는 클래스를 상속받으면 자동으로 만들어짐

class Rabbit extends Animal {
  constructor(...args) {
    super(...args);
  }
}
1
2
3
4
5

상속 클래스의 생성자에서

  • super 을 호출하지 않으면 에러가 발생한다
  • super 를 this 를 사용하기 전에 호출하여야 한다.

상속 클래스의 생성자에서 super 를 호출하지 않으면 에러가 발생하는 이유

  • 다른점 : 상속 클래스의 생성자 함수의 내부 프로퍼티 [[ConstructorKind]]:"derived"
  1. 일반 클래스가 new 와 함께 실행 -> 빈 객체가 만들어지고 & this 에 이 객체를 할당함
  2. 상속 클래스의 생성자 함수가 실행 -> 빈 객체를 만들고 & this 에 객체를 할당하는 일은 수퍼클래스의 생성자가 처리해 줘야한다.
    • 상속 클래스의 생성자에서 super(...) 수퍼클래스의 생성자를 실행해 줘야 this 객체가 만들어진다.

# extends 키워드

클래스의 두 개의 프로토타입을 설정한다.

  1. 일반 메서드용 생성자함수의 'prototype' 사이
  2. 정적 메서드용 생성자함수 자체 사이
class Rabbit extends Object {}

alert( Rabbit.prototype.__proto__ === Object.prototype ); // (1) true
alert( Rabbit.__proto__ === Object ); // (2) true
1
2
3
4

# super 의 내부 동작

# 1. super 를 구현하려는 시도

this.__proto__.eat.call(this); 을 주목해서 보자

let animal = {
  name: "동물",
  eat() {
    alert(`${this.name} 이/가 먹이를 먹습니다.`);
  }
};

let rabbit = {
  __proto__: animal,
  name: "토끼",
  eat() {
    // 예상대로라면 super.eat()이 동작해야 합니다.
    this.__proto__.eat.call(this); // (*)
  }
};

rabbit.eat(); // 토끼 이/가 먹이를 먹습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

체인에 객체를 하나더 추가했을 때 무한루프가 발생한다.

let longEar = {
  __proto__: rabbit,
  eat() {
    // longEar를 가지고 무언가를 하면서 부모(rabbit) 메서드를 호출합니다.
    this.__proto__.eat.call(this); // (**)
  }
};

longEar.eat(); // RangeError: Maximum call stack size exceeded
1
2
3
4
5
6
7
8
9

그 이유는 (*), (**) 를 보자 longEar.eat(); 를 호출했을때,
this.__proto__.eat.call(this); // (**) 에서 this.__proto__ 는 Rabbit 이다.
this.__proto__.eat.call(this); // (*) 에서의 맨 앞의 this 는 (**) 에 의해서 longEar 로 바인딩 되었기 때문에 역시 this.__proto__ 는 Rabbit 이다. image

# 2. super[[HomeObject]] 를 통해 수퍼 프로토타입과 메서드를 찾는다.

[[HomeObject]] : 함수 특수 프로퍼티

  • (클래스 || 객체) 메서드 함수의 [[HomeObject]] 에 해당 객체가 저장된다.

따라서 [[HomeObject]] 를 알고 있기 때문에 this 없이도 프로토타입으로부터 부모 메서드를 가져올 수 있다.

let animal = {
  name: "동물",
  eat() {         // animal.eat.[[HomeObject]] == animal
    alert(`${this.name} 이/가 먹이를 먹습니다.`);
  }
};

let rabbit = {
  __proto__: animal,
  name: "토끼",
  eat() {         // rabbit.eat.[[HomeObject]] == rabbit
    super.eat();
  }
};

let longEar = {
  __proto__: rabbit,
  name: "귀가 긴 토끼",
  eat() {         // longEar.eat.[[HomeObject]] == longEar
    super.eat();
  }
};

// 이제 제대로 동작합니다
longEar.eat();  // 귀가 긴 토끼 이/가 먹이를 먹습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

객체 메서드에서도 super 를 쓸수 있구나.

# 3. [[HomeObject]] 는 변경될 수 없다.

[[HomeObject]]super 내부에서만 유효하다.

let animal = {
  sayHi() {
    console.log(`나는 동물입니다.`);
  }
};

// rabbit은 animal을 상속받습니다.
let rabbit = {
  __proto__: animal,
  sayHi() {
    super.sayHi();
  }
};

let plant = {
  sayHi() {
    console.log("나는 식물입니다.");
  }
};

// tree는 plant를 상속받습니다.
let tree = {
  __proto__: plant,
  sayHi: rabbit.sayHi // (*)
};

tree.sayHi();  // I'm an animal (?!?)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
  1. (*) : tree.sayHi - 중복 코드를 방지하기 위해 rabbit의 메서드를 복사
  2. 복사한 메서드의 [[HomeObject]] 는 여전히 rabbit 이기 때문에 animalsayHi를 찾는다. image
  • super 가 없는 메서드는 객체간 복사가 잘 될 수 있다.

# 4. [[HomeObject]] 메서드로 정의해야 가질 수 있다.

  • method() O
  • method: function() X
let animal = {
  eat: function() { // 'eat() {...' 대신 'eat: function() {...'을 사용해봅시다.
    // ...
  }
};

let rabbit = {
  __proto__: animal,
  eat: function() {
    super.eat();
  }
};

rabbit.eat();  // SyntaxError: 'super' keyword unexpected here ([[HomeObject]]가 없어서 에러가 발생함)
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 정적 프로퍼티, 메서드의 상속

정적 프로퍼티와 메서드는 프로토타입 체인에 의해 상속 된다. image

class Animal {}
class Rabbit extends Animal {}

// 정적 메서드
alert(Rabbit.__proto__ === Animal); // true

// 일반 메서드
alert(Rabbit.prototype.__proto__ === Animal.prototype); // true
1
2
3
4
5
6
7
8

정적 메서드 상속을 사용한 예

class Rabbit extends Object {}

// 보통은 Object.getOwnPropertyNames 로 호출합니다.
alert ( Rabbit.getOwnPropertyNames({a: 1, b: 2})); // a,b
1
2
3
4

extends Object가 없을 때

class Rabbit {}
alert(Rabbit.__proto__ === Object); // false
alert(Rabbit.getOwnPropertyNames({ a: 1, b: 2 });   // Error

alert(Object.__proto__ === Function.prototype);   // true
alert(Rabbit.__proto__ === Function.prototype); // true
alert(Rabbit.prototype.__proto__ === Object.prototype); // true
alert(Rabbit.__proto__ === Object.__proto__);   // true
1
2
3
4
5
6
7
8

Function.prototype

  • call, bind 등의 일반함수 메서드를 가진다.
  • 모든 함수의 기본 프로토타입
class Rabbit class Rabbit extends Object
- 생성자에서 super() 를 만드시 호출해야함
Rabbit.__proto__ === Function.prototype Rabbit.__proto__ === Object

image

# private, protected 프로퍼티와 메서드

# 객체지향 프로그래밍 내부 & 외부 인터페이스

복잡한 애플리케이션을 구현하려면, 내부 인터페이스와 외부 인터페이스를 구분하는 방법을 ‘반드시’ 알고 있어야 한다.

내부 인터페이스

  • 동일한 클래스 내의 다른 메서드에서 접근 가능.
  • 클래스 밖에서 접근할 수 없는 프로퍼티와 메서드.

외부 인터페이스

  • 클래스 밖에서도 접근 가능한 프로퍼티와 메서드

# 자바스크립트의 타입

public

  • 외부 인터페이스 구성 private
  • 내부 인터페이스 구성
  • 클래스 내부에서만 접근가능

자바스크립트는 protected 를 지원하지 않지만 모방해서 만들 수 있다.

  • private 과 비슷하지만 서브 클래스에서 접근 가능하다.
  • 내부 인터페이스 구성

# 프로퍼티 보호하기

예시) 커피 머신 클래스

public waterAmount, power

class CoffeeMachine {
  waterAmount = 0; // 물통에 차 있는 물의 양

  constructor(power) {
    this.power = power;
    alert( `전력량이 ${power}인 커피머신을 만듭니다.` );
  }
}

// 커피 머신 생성
let coffeeMachine = new CoffeeMachine(100);

// 물 추가
coffeeMachine.waterAmount = 200;
1
2
3
4
5
6
7
8
9
10
11
12
13
14

public waterAmount 의 값을 제한하기

class CoffeeMachine {
  _waterAmount = 0;

  set waterAmount(value) {
    if (value < 0) throw new Error("물의 양은 음수가 될 수 없습니다.");
    this._waterAmount = value;
  }

  get waterAmount() {
    return this._waterAmount;
  }

  constructor(power) {
    this._power = power;
  }

}

// 커피 머신 생성
let coffeeMachine = new CoffeeMachine(100);

// 물 추가
coffeeMachine.waterAmount = -10; // Error: 물의 양은 음수가 될 수 없습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 읽기 전용 프로퍼티

setter 는 만들지 않고 getter 만 만들어야 한다.

class CoffeeMachine {
  // ...

  constructor(power) {
    this._power = power;
  }

  get power() {
    return this._power;
  }
}

// 커피 머신 생성
let coffeeMachine = new CoffeeMachine(100);

alert(`전력량이 ${coffeeMachine.power}인 커피머신을 만듭니다.`); // 전력량이 100인 커피머신을 만듭니다.

coffeeMachine.power = 25; // Error (setter 없음)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# private 프로퍼티

  • 클래스 외부나 서브 클래스에서 접근할 수 없다.
  • this[name] 필드 보안강화를 위해 이 문법은 쓸 수 없다.
class CoffeeMachine {
  #waterLimit = 200;

  #checkWater(value) {
    if (value < 0) throw new Error("물의 양은 음수가 될 수 없습니다.");
    if (value > this.#waterLimit) throw new Error("물이 용량을 초과합니다.");
  }
}

let coffeeMachine = new CoffeeMachine();

// 클래스 외부에서 private에 접근할 수 없음
coffeeMachine.#checkWater(); // Error
coffeeMachine.#waterLimit = 1000; // Error
1
2
3
4
5
6
7
8
9
10
11
12
13
14

한 클래스 내에서 public 프로퍼티와 동일한 이름의 private 프로퍼티를 가질 수 있다. private 프로퍼티를 이용하려면 getter 와 setter 를 사용하는 방법이 있다.

class CoffeeMachine {

  #waterAmount = 0;

  get waterAmount() {
    return this.#waterAmount;
  }

  set waterAmount(value) {
    if (value < 0) throw new Error("물의 양은 음수가 될 수 없습니다.");
    this.#waterAmount = value;
  }
}

let machine = new CoffeeMachine();

machine.waterAmount = 100;
alert(machine.#waterAmount); // Error
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# private 필드와 메서드의 상속

private 필드와 메서드는 상속되지 않는다.
하위 클래스에서 상위 클래스의 필드와 메소드에 접근하기 위해선 protected 라는 접근제한자를 사용합니다.
그런데 JS에는 private만 있고 protected가 없기 때문에 private으로 선언된 것은 아예 상속되지 않습니다.

# 클래스 상속과 프로토타입 상속

클래스 상속은 내부적으로 프로토타입 상속을 활용한다.

클래스 상속에 대한 프로토 타입 체인이다.

class Person {}
class Student extends Person {}
const student = new Student();
1
2
3

image 프로토타입 기반 객체 지향 프로그래밍


  1. Animal 클래스 생성
class Animal {
  constructor(name) {
    this.speed = 0;
    this.name = name;
  }
  run(speed) {
    this.speed = speed;
    alert(`${this.name} 은/는 속도 ${this.speed}로 달립니다.`);
  }
  stop() {
    this.speed = 0;
    alert(`${this.name} 이/가 멈췄습니다.`);
  }
}

let animal = new Animal("동물");
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

image

  1. Animal Class 를 확장한 Rabbit Class 생성
class Rabbit extends Animal {
  hide() {
    alert(`${this.name} 이/가 숨었습니다!`);
  }
}

let rabbit = new Rabbit("흰 토끼");

rabbit.run(5); // 흰 토끼 은/는 속도 5로 달립니다.
rabbit.hide(); // 흰 토끼 이/가 숨었습니다!
1
2
3
4
5
6
7
8
9
10

image

run 을 찾을 때 프로토타입 체인을 따라서 Animal 에서 run 을 찾는다고 기술되어 있는데, 아래 ES5 와 ES6 를 비교하는 내용에서는

# ES5로 Class를 구현

# ES6 Class 와 ES5 prototype 상속 의 차이

# new.target 중심으로 개편된 es6 객체 생성 시스템

# constructor

# new 키워드

class 의 constructor 는 new 명령어 없이 호출할 수 없다.

function ES5(name) {
  this.name = name;
  return name + ' es5';
}
class ES6 {
  constructor(name) {
    this.name = name;
    return name + ' es6';
  }
}
console.log(ES5('ES5'));                        // ES5 es5
console.log(ES5.prototype.constructor('ES5'));  // ES5 es5
console.log(ES6('ES6'));                        // Uncaught TypeError
console.log(ES6.prototype.constructor('ES6'));  // Uncaught TypeError
1
2
3
4
5
6
7
8
9
10
11
12
13
14

class 로 만든 함수엔 특수 내부 프로퍼티 [[FunctionKind]]:"classConstructor 가 붙는다. JS 엔진은 함수에 [[FunctionKind]]:"classConstructor 가 있을 때 new 와 함께 호출하지 않으면 에러가 발생한다.

# super & extends 키워드

function SuperClass() {
  this.a = 1;
}
function SubClass() {
  this.b = 2;
}
SubClass.prototype = new SuperClass();         // 수퍼 클래스의 인스턴스 상속
SubClass.prototype.constructor = SubClass;
const obj = new SubClass();
console.log(obj.a, obj.b);              // 1 2
console.log(obj.hasOwnProperty('a'));   // false
console.log(obj.hasOwnProperty('b'));   // true
1
2
3
4
5
6
7
8
9
10
11
12

서브 클래스에서 수퍼 클래스의 생성자를 호출할 수 없다. 프로토타입 체이닝을 통한 수퍼 클래스 생성자 프로퍼티의 전달이 되었기 때문에 hasOwnProperty('a') 의 결과는 false 이다.


만약 수퍼클래스의 프로퍼티를 전달하는 것이 아닌, 서브 클래스의 프로퍼티로 만들기 위해서는 아래와 같은 복잡한 방법을 사용해야 한다.

function SuperClass(){
  this.a = 1;
}
function SubClass(){
  const parentObj = Object.getPrototypeOf(this);
  for(let i in parentObj){
    this[i] = parentObj[i];
  }
  this.b = 2;
}
SubClass.prototype = new SuperClass();  // 수퍼 클래스의 인스턴스 상속
SubClass.prototype.constructor = SubClass;
const obj = new SubClass();
console.log(obj.a, obj.b);              // 1 2
console.log(obj.hasOwnProperty('a'));   // true
console.log(obj.hasOwnProperty('b'));   // true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

하지만 이 방법 또한 SubClass 에서 SuperClass 의 속성을 가지기 위해서
SuperClass 의 생성자를 호출하였기 때문에 SubClass.prototype = new SuperClass();
메모리 낭비가 있을 수 있다.

console.log(Object.getPrototypeOf(obj).a);  // 1
console.log(Object.getPrototypeOf(obj).hasOwnProperty('a'));  // true
1
2

ES6 Class

class SuperClass {
  constructor(){
    this.a = 1;
  }
}
class SubClass extends SuperClass {
  constructor(){
    super();
    this.b = 2;
  }
}
const obj = new SubClass();
console.log(obj.a, obj.b);              // 1 2
console.log(obj.hasOwnProperty('a'));   // true
console.log(obj.hasOwnProperty('b'));   // true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

서브클래스

  • 수퍼클래스의 인스턴스를 상속받지 않는다.
    • cf. 수퍼 클래스의 인스턴스를 상속받는 예) SubClass.prototype = new SuperClass();
  • 수퍼클래스의 메서드를 상속 받는다

서브 클래스의 인스턴스의 프로토타입 체인상에서도 수퍼 클래스의 constructor 의 실행결과는 존재하지 않는다.

console.log(Object.getPrototypeOf(obj).a);  // undefined
console.log(Object.getPrototypeOf(obj).hasOwnProperty('a'));  // false
1
2

ES5 에서 위와같은 기능을 구현하기 위해서는 프록시를 활용해야 한다.

function SuperClass() { this.a = 1; }
function SubClass() { this.a = 2; }
function Proxy() {}
Proxy.prototype = new SuperClass();
SubClass.prototype.constructor = SubClass;
SubClass.superClass = SuperClass.prototype;
1
2
3
4
5
6

내가 이해한 바로는 Proxy 의 prototype 속성에 임시로 SuperClass 의 인스턴스를 생성하여 할당하고. SuperClass 의 실제 프로토타입 메서드 만을 SubClass 에게 상속해 주는 역할이 아닐까 싶다.

let inherit = (function () {
    function F() {}
    return function(Sub, Super) {
        F.prototype = Super.prototype;
        Sub.prototype = new F();
        Sub.constructor.prototype = Sub;
        Sub.superClass = Super.prototype;
    }
})();
function Super() { this.a = 1;}
function Sub() { this.a = 2 };
inherit(Sub, Super);
1
2
3
4
5
6
7
8
9
10
11
12

# Methods

# static method & method 의 상속

ES5

function SuperClass() { }
SuperClass.staticMethod = function() {
  this.s = 11;
  return 'static method';
};
SuperClass.prototype.method = function() {
  this.m = 12;
  return 'method';
};
function SubClass() { }
SubClass.prototype = new SuperClass();
SubClass.prototype.constructor = SubClass;
let obj = new SubClass();
1
2
3
4
5
6
7
8
9
10
11
12
13

ES6 Class

class SuperClass {
  static staticMethod() {
    this.s = 11;
    return 'static method';
  }
  method() {
    this.m = 12;
    return 'method';
  }
}
class SubClass extends SuperClass { }
const obj = new SubClass();
1
2
3
4
5
6
7
8
9
10
11
12

method 의 출력 결과는 서브 클래스 , 수퍼클래스 모두 같다.

console.log(obj.method());                   // 'method'
console.log(SubClass.prototype.method());       // 'method'
console.log(SuperClass.prototype.method());      // 'method'
1
2
3

프로토타입 체인에 의해 결과는 모두 같다.


그러나 static method 의 경우는 다르다.
인스턴스에서 static 메서드를 호출할 수 없는 것은 당연하다.
static 은 인스턴스의 메서드가 아니라 생성자의 메서드 이기 때문이다.

ES5, static method 는 수퍼 클래스에서만 호출이 가능하다.

console.log(obj.staticMethod());             // Uncaught TypeError
console.log(SubClass.staticMethod());           // Uncaught TypeError
console.log(SuperClass.staticMethod());          // 'static'
1
2
3

그 이유는 생성자는 프로토타입 체인으로 연결되어 있지 않았고, static 메서드는 생성자의 메서드 이기 때문이라고 생각한다.

function SubClass() { }
SubClass.prototype = new SuperClass();
SubClass.prototype.constructor = SubClass;
1
2
3

ES6, static method 는 수퍼 클래스와 서브클래스 모두에서 호출 가능하다.

console.log(obj.staticMethod());             // Uncaught TypeError
console.log(SubClass.staticMethod());           // 'static'
console.log(SuperClass.staticMethod());          // 'static'
1
2
3

그렇다면 ES5 에서 ES6 와 같은 결과를 얻기 위해서는 다음과 같이 method 들을 모두 복사해 주면 된다.

const inherit = (function() {
  function F(){ }
  return function(Sub, Super) {
    F.prototype = Super.prototype;
    Sub.prototype = new F();
    Sub.constructor.prototype = Sub;
    Sub.superClass = Super.prototype;
    for(const static in Super) {
      Sub['super_' + static] = Super[static];
    }
  }
})();
function SuperClass() { this.a = 1; }
SuperClass.method = function(){ console.log('super static'); };
function SubClass() { this.a = 2; }
SubClass.method = function(){ console.log('sub static'); };
inherit(SubClass, SuperClass);
SubClass.method();                         // sub static method
SubClass.super_method();                   // super static method
SubClass.superClass.constructor.method();  // super static method
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

Sub['super_' + static] = Super[static]; 은 서브클래스의 중복된 메서드명을 피하기 위해 접두사를 붙여 주었다.

# 생성자 함수

ES5

  • static method, Method 는 함수이기 때문에 생성자 함수로 사용할 수 있다.
const methodObj = new ES5Super.prototype.method();
const staticObj = new ES5SUper.staticMethod();
1
2

ES6 Class

  • static method, Method 는 생성자 함수로 사용될 수 없다
  • ES6 static method
    • arguments, caller 값이 노출되지 않는다.
    • name 이 자동으로 지정된다.
    • prototype 이 없다.
  • ES6 의 shorthand method
    • function 에서 많은 기능이 제한되어 오직 method 로서만 사용할 수 있는 특수 함수.

# superClass 메서드 차용

ES5

function SuperClass() { }
SuperClass.prototype.method = function() {
  return 'super';
};
function SubClass() { }
SubClass.prototype = new SuperClass();
SubClass.prototype.constructor = SubClass;
SubClass.prototype.method = function() {    // 수퍼 클래스와 서브클래스에 동일한 메서드를 정의한 경우.
  return Object.getPrototypeOf(this).method() + ' sub';
};
const obj = new SubClass();
console.log(obj.method());      // 'super sub sub'
1
2
3
4
5
6
7
8
9
10
11
12

'super sub sub' 의 결과가 나온 이유

인스턴스에서 method를 호출하면 __proto__에 위치한 메소드를 마치 인스턴스 자신의 것처럼 사용하기 때문에, 위 메소드에는 최초 실행시 this에는 ‘obj’가 할당되었다가, 재귀적으로 SubClass.prototype가 할당되었다가, 다시 SuperClass.prototype이 할당되기 때문에, 본래 의도한 ‘super sub’라는 결과 대신 ‘ sub’가 한 번 더 출력되고 말았습니다.

이해가 잘 안된다.

function SuperClass() { }
SuperClass.prototype.method = function() {
    return 'super';
};
function SubClass() { }
SubClass.prototype = new SuperClass();
SubClass.prototype.constructor = SubClass;
SubClass.prototype.method = function() {
    const df = 1;
    const callSuper = Object.getPrototypeOf(this).method();
    const result = callSuper + ' sub';
    return result;
};
var obj = new SubClass();
console.log(obj.method());
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

단계를 쪼개어서 디버그를 돌려보았다.

문제의 코드이다. const callSuper = Object.getPrototypeOf(this).method();

코드 실행순서를 보면

  1. this.method(this: SubClass, SubClass.prototype.method) 을 먼저 호출
  2. this 가 Super 클래스인 SubClass.prototype.method 메서드가 다시 호출

const df = 1; 코드를 통해서 재 호출 된 것임을 영상을 통해 확인할 수 있다.

  1. this.method(this: SuperClass, SuperClass.prototype.method)를 성공적으로 마친후에 const result = super sub; 가 된 것을 확인할 수 있었다.

  2. return 된 result 는 문맥(this)이 SuperClass 에서 SubClass 으로 전환되면서 const callSuper = 'super sub'; 부터 다시 실행 되었다.

~~
처음 예상했을 때는 SuperClass.method 를 바로 호출할 거라고 생각했었다.

이런 코드의 흐름을 갖게된 원인이 무엇일까? getPrototypeOf this 에 있는걸까?

this 가 getPrototypeOf 에게 wrapper 되기 전에 실행이 된걸까? 비동기적인 문제의 영향도 있는걸까?

const Super = SuperClass.prototype.method(); // "super"
const Sub = SubClass.prototype.method();  // Super + "sub"
child.method() // Sub + "sub" => super sub sub
1
2
3

이런식의 콜백이 발생했다는 것은 알게 되었는데 왜 이런식의 콜백을 발생시키는 코드는 어떻게 생겼는지 알수가 없다.

자바스크립트의 이런 엉뚱한 측면은 흥미롭다.


this 를 사용할 경우 ES6 의 클래스에서도 똑같이 확인된다.

class SuperClass {
  method() {
    return 'super';
  }
}
class SubClass extends SuperClass {
  method() {
    return Object.getPrototypeOf(this).method() + ' sub';
  }
}
let obj = new SubClass();
console.log(obj.method());      // 'super sub sub' 
1
2
3
4
5
6
7
8
9
10
11
12

ES6 의 super 키워드는 this 활용시의 문제점을 피해갈 수 있다.

class SuperClass {
  method() {
    return 'super';
  }
}
class SubClass extends SuperClass {
  method() {
    return super.method() + ' sub';
  }
}
var obj = new SubClass();
console.log(obj.method());      // 'super sub'
1
2
3
4
5
6
7
8
9
10
11
12

super 키워드는 상위 클래스만을 가리키므로 재귀적으로 여러번 호출될 염려가 없다.

# enumerable 열거형

클래스 메서드는 열거할 수 없다. enumerable: false

for..in 으로 순회할 때, 메서드는 순회 대상에서 제외하고 멤버 변수만 순회할 수 있다.

# hoisting

# ReferenceError

ES5

const es5 = new ES5();
function ES5() {
  this.a = 1;
}
console.log(es5);    // ES5 {a: 1}
1
2
3
4
5

ES6 : Uncaught ReferenceError

const es6 = new ES6();
class ES6 {
  constructor() {
    this.a = 1;
  }
}
console.log(es6);  // Uncaught ReferenceError
1
2
3
4
5
6
7

# Block Scope

ES5 : 엄격모드에서만 block scope 영향을 받는다.

function A(){ this.a = 1; }
{
  function A(){ this.a = 2; }
  console.log(new A());    // A {a: 2}
}
console.log(new A());      // A {a: 2}
1
2
3
4
5
6
'use strict';
function A(){ this.a = 1; }
{
  function A(){ this.a = 2; }
  console.log(new A());    // A {a: 2}
}
console.log(new A());      // A {a: 1}
1
2
3
4
5
6
7

ES6 : 항상 block scope 에 종속된다.

class A {
  constructor(){ this.a = 1; }
}
{
  class A {
    constructor(){ this.a = 2; }
  }
  console.log(new A());    // A {a: 2}
}
console.log(new A());      // A {a: 1}
1
2
3
4
5
6
7
8
9
10

# 엄격모드

클래스는 항상 엄격 모드로 실행 된다.

# 타입 확인

# instanceof 로 클래스 확인하기

  • 객체가 특정 클래스에 속하는지 확인 상속 관계 확인
  • obj instanceof Class : true
    • obj 가 Class 에 속할 때
    • Class 를 상속받는 클래스에 속할 때
    • new Class() instanceof Class

# 프로토타입 체인과 instanceof

보통, 프로토타입 체인을 거슬러 올라가며 인스턴스 여부나 상속 여부를 확인한다.

let arr = [1, 2, 3];
alert( arr instanceof Array ); // true
alert( arr instanceof Object ); // true
1
2
3

# instanceof 알고리즘

# 1. 클래스에 정적메서드 Symbol.hasInstance 구현여부 확인

obj instanceof Class문이 실행될 때, Class[Symbol.hasInstance](obj)가 호출된다.

  • 호출 결과는 boolean 이다.
  • 직접 확인 로직을 설정할 수 있다.
  • 대부분의 클래스엔 구현되어 있지 않다.

canEat 프로퍼티가 있으면 animal 이라고 판단할 수 있도록 instanceOf 의 로직을 직접 설정합니다.

class Animal {
  static [Symbol.hasInstance](obj) {
    if (obj.canEat) return true;
  }
}

let obj = { canEat: true };

alert(obj instanceof Animal); // true, Animal[Symbol.hasInstance](obj)가 호출됨
1
2
3
4
5
6
7
8
9

# 2. Class.prototype 이 obj 프로토타입 체인 상의 프로토토타입 중 하나와 일치하는지 확인

obj.__proto__ === Class.prototype?
obj.__proto__.__proto__ === Class.prototype?
obj.__proto__.__proto__.__proto__ === Class.prototype?
...
1
2
3
4
  • true : 이 중 하나라도 true
  • false: 그렇지 않고 체인의 끝에 도달 image

# objA.isPrototypeOf(objB) 🔗

  • objA가 objB의 프로토타입 체인 상 어딘가에 있으면 true 를 반환해주는 메서드
  • instanceof 연산자와 다른점
    • "object instanceof AFunction"
      • object 의 프로토타입 체인을 AFunction 자체가 아니라 AFunction.prototype 에 대해 확인 한다.
    • Class 생성자를 제외하고 포함 여부를 검사 한다는 것이다. (?)
function Rabbit() {}
let rabbit = new Rabbit();

// 프로토타입이 변경됨
Rabbit.prototype = {};

// 더 이상 Rabbit이 아닙니다!
alert( rabbit instanceof Rabbit ); // false
1
2
3
4
5
6
7
8
function A() {}
function B() {}

A.prototype = B.prototype = {};

let a = new A();

alert( a instanceof B ); // true
1
2
3
4
5
6
7
8

문제에서 a.proto == B.prototype 이므로, instanceof 는 true 를 반환.

# Object.prototype.toString

타입 확인을 위한 Object.prototype.toString

let obj = {};

alert(obj); // [object Object]
alert(obj.toString()); // 같은 결과가 출력됨
1
2
3
4

일반 객체를 문자열로 변환했을 때 [object Object] 가 출력되는 이유

  • toString 의 구현방식 때문
  • typeof, instanceof 의 대안을 만들 수 있다.
let objectToString = Object.prototype.toString;
let arr = [];

alert( objectToString.call(arr) ); // [object Array]
1
2
3
4
  • 숫자형 – [object Number]
  • 불린형 – [object Boolean]
  • null – [object Null]
  • undefined – [object Undefined]
  • 배열 – [object Array]
  • 그외 – 커스터마이징 가능

# Symbol.toStringTag

  • 특수 객체 프로퍼티
  • toString 커스터마이징
let user = {
  [Symbol.toStringTag]: "User"
};

alert( {}.toString.call(user) ); // [object User]
1
2
3
4
5

특정 호스트 환경의 객체와 클래스에 구현된 toStringTag

alert( window[Symbol.toStringTag]); // Window
alert( XMLHttpRequest.prototype[Symbol.toStringTag] ); // XMLHttpRequest

alert( {}.toString.call(window) ); // [object Window]
alert( {}.toString.call(new XMLHttpRequest()) ); // [object XMLHttpRequest]
1
2
3
4
5
- 동작 대상 반환값
typeof 원시형 문자열
{}.toString 원시형, 내장 객체, Symbol.toStringTag 가 있는 객체 문자열
instanceof 객체, 계층 구조를 가진 클래스를 다룰 때, 클래스의 상속 여부를 확인 true/false

# 믹스인

다른 클래스를 상속받을 필요 없이, 이들 클래스에 구현되어있는 메서드를 담고 있는 클래스.

자바스크립트는 다중 상속을 지원하지 않지만 믹스인을 사용하면 메서드를 복사해 프로토타입에 구현할 수 있다.

그러나, 믹스인이 실수로 기존 클래스 메서드를 덮어쓰는 일이 없도록 해야한다.

  • 특정 행동을 실행해주는 메서드를 제공
  • 단독으로 쓰이지 않고 다른 클래스에 행동을 더해주는 용도.

# 믹스인 사용법

믹스인

let sayHiMixin = {
  sayHi() {
    alert(`Hello ${this.name}`);
  },
  sayBye() {
    alert(`Bye ${this.name}`);
  }
};
1
2
3
4
5
6
7
8
class User {
  constructor(name) {
    this.name = name;
  }
}

// 메서드 복사
Object.assign(User.prototype, sayHiMixin);

// 이제 User가 인사를 할 수 있습니다.
new User("Dude").sayHi(); // Hello Dude!
1
2
3
4
5
6
7
8
9
10
11

# 믹스인 안에서 믹스인 상속

let sayMixin = {
  say(phrase) {
    alert(phrase);
  }
};

let sayHiMixin = {
  __proto__: sayMixin, // (Object.create를 사용해 프로토타입을 설정할 수도 있습니다.)

  sayHi() {
    // 부모 메서드 호출
    super.say(`Hello ${this.name}`); // (*)
  },
  sayBye() {
    super.say(`Bye ${this.name}`); // (*)
  }
};

class User {
  constructor(name) {
    this.name = name;
  }
}

// 메서드 복사
Object.assign(User.prototype, sayHiMixin);

// 이제 User가 인사를 할 수 있습니다.
new User("Dude").sayHi(); // Hello Dude!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

image

# 이벤트 믹스인

클래스나 객체에 이벤트 관련 함수를 쉽게 추가할 수 있는 믹스인

# 용도1

  • 사용자 로그인에서, 객체 userlogin 이벤트 생성
  • calendaruser 가 생성한 login 이벤트를 듣고 사용자에게 맞는 달력을 제공

# 용도2

  • 메뉴의 항목을 선택했을 때 객체 menuselect 라는 이벤트를 생성
  • 어떤 객체가 select 에 반응하는 이벤트 핸들러 할당

# 사용법

믹스인

let eventMixin = {
  /**
   *  이벤트 구독
   *  eventName 이벤트가 발생하면 handler 함수를 할당한다.
   *  한 이벤트에 대응하는 여러 핸들러가 있을 때
   *  사용패턴: menu.on('select', function(item) { ... }
  */
  on(eventName, handler) {
    if (!this._eventHandlers) this._eventHandlers = {};
    if (!this._eventHandlers[eventName]) {
      this._eventHandlers[eventName] = [];
    }
    this._eventHandlers[eventName].push(handler);
  },

  /**
   *  구독 취소
   *  핸들러 리스트에서 handler 를 제거한다.
   *  사용패턴: menu.off('select', handler)
   */
  off(eventName, handler) {
    let handlers = this._eventHandlers?.[eventName];
    if (!handlers) return;
    for (let i = 0; i < handlers.length; i++) {
      if (handlers[i] === handler) {
        handlers.splice(i--, 1);
      }
    }
  },

  /**
   *  주어진 이름과 데이터를 기반으로 이벤트 생성
   *  
   *  사용패턴: this.trigger('select', data1, data2);
   */
  trigger(eventName, ...args) {
    if (!this._eventHandlers?.[eventName]) {
      return; // no handlers for that event name
    }

    // 핸들러 호출
    this._eventHandlers[eventName].forEach(handler => handler.apply(this, args));
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 클래스 생성
class Menu {
  choose(value) {
    this.trigger("select", value);
  }
}
// 이벤트 관련 메서드가 구현된 믹스인 추가
Object.assign(Menu.prototype, eventMixin);

let menu = new Menu();

// 메뉴 항목을 선택할 때 호출될 핸들러 추가
menu.on("select", value => alert(`선택된 값: ${value}`));

// 이벤트가 트리거 되면 핸들러가 실행되어 얼럿창이 뜸
// 얼럿창 메시지: Value selected: 123
menu.choose("123");
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# Reference