Skip to content

Latest commit

 

History

History
898 lines (729 loc) · 26.7 KB

6주차.md

File metadata and controls

898 lines (729 loc) · 26.7 KB

1. 클래스와 기본 문법
2. 클래스 상속
3. 정적 메서드와 정적 프로퍼티
4. private, protected 프로퍼티와 메서드
5. 내장 클래스 확장하기
6. 'instanceof'로 클래스 확장하기
7. 믹스인


클래스와 기본 문법

  • 동일한 객체를 여러개 생성해야 할 때 생성자 함수 외에 클래스를 사용해서 생성할 수 있음
     class MyClass {
       // 여러 메서드를 정의할 수 있음
       constructor() { ... } // 객체가 생성될 때 초기 상태를 설정
       method1() { ... }
       method2() { ... }
       method3() { ... }
       ...
     }
    

  • 클래스는 함수의 한 종류
     class User {
       constructor(name) { this.name = name; }
       sayHi() { alert(this.name); }
     }
     
     // User가 함수라는 증거
     alert(typeof User); // function
    
    1. User 이라는 이름을 가진 함수를 생성. 이때 생성자 메서드 constructor에서 가져옴. 생성자 메서드가 없으면 본문이 비워진 채로 함수가 만들어짐
    2. sayHi 같은 클래스 내에서 정의한 메서드를 User.prototype에 저장
  • new User를 호출해 객체를 만들고, 객체의 메서드를 호출하면 메서드는 prototype 프로퍼티에 저장되어 있기 때문에 prototype 프로퍼티를 통해 가져옴 (함수의 prototype 프로퍼티)
     class User {
       constructor(name) { this.name = name; }
       sayHi() { alert(this.name); }
     }
     
     // 클래스는 함수
     alert(typeof User); // function
     
     // 정확히는 생성자 메서드와 동일
     alert(User === User.prototype.constructor); // true
     
     // 클래스 내부에서 정의한 메서드는 User.prototype에 저장됨
     alert(User.prototype.sayHi); // alert(this.name);
     
     // 현재 프로토타입에는 메서드가 두 개
     alert(Object.getOwnPropertyNames(User.prototype)); // constructor, sayHi
    

  • class라는 키워드 없이도 클래스 역할을 하는 함수를 선언할 수 있기 땜문에 클래스는 '편의 문법'에 불과하다라고 생각하는 사람도 존재
    • 편의 문법이란? 기능은 동일하나 기존 문법을 쉽게 읽을 수 있게 만든 문법
     // 생성자 함수를 통해 class User와 동일한 기능을 구현
     
     // 1. 생성자 함수를 만듭니다.
     function User(name) {
       this.name = name;
     }
     // 모든 함수의 프로토타입은 'constructor' 프로퍼티를 기본으로 갖고 있기 때문에
     // constructor 프로퍼티를 명시적으로 만들 필요가 없음
     // F.prototype = { constructor: F }
     
     // 2. prototype에 메서드를 추가
     User.prototype.sayHi = function() {
       alert(this.name);
     };
     
     // 사용법:
     let user = new User("John");
     user.sayHi();
    

  • 생성자 함수나 클래스로 만든 함수나 결과는 거의 같지만 몇몇 차이점이 존재
    1. class로 만든 함수엔 특수 내부 프로퍼티인 [[IsClassConstructor]]: true가 존재
      • 클래스 생성자를 new와 함께 호출하지 않으면 에러가 발생하는데 이때 [[IsClassConstructor]]: true가 사용됨
         class User {
           constructor() {}
         }
         
         alert(typeof User); // User의 타입은 함수이긴 하지만 그냥 호출할 수 없음
         User(); // TypeError: Class constructor User cannot be invoked without 'new'
        
      • 문자열로 형변환하면 'class...'로 시작하는 문자열이 되는데 이때도 [[IsClassConstrucotr]]: true가 사용됨
    2. 클래스에 정의된 메서드는 열거할 수 없음(non-enumerable). 클래스의 prototype 프로퍼티에 추가된 메서드의 enumerable 플래그는 false
    3. 클래스는 항상 엄격 모드로 실행됨(use strict). 클래스 생성자 안 코드 전체엔 자동으로 엄격 모드가 적용됨

  • 클래스를 표현식으로 구현할 수 있음. 기명 함수 표현식과 유사하게 클래스 표현식에도 이름을 붙일 수 있음(기명 클래스 표현식)
    • 이 이름은 오직 클래스 내부에서만 사용 가능
     // 기명 클래스 표현식(Named Class Expression)
     // (명세서엔 없는 용어이지만, 기명 함수 표현식과 유사하게 동작)
     let User = class MyClass { // 클래스 이름 MyClass
       sayHi() {
         alert(MyClass); // MyClass라는 이름은 오직 클래스 안에서만 사용 가능
       }
     };
     
     new User().sayHi(); // 원하는대로 MyClass의 정의를 보여줌
     
     alert(MyClass); // ReferenceError: MyClass is not defined, MyClass는 클래스 밖에서 사용할 수 없음
    

  • getter(획득자)나 setter(설정자) 구현 가능
     class User {
       constructor(name) {
         // setter를 활성화
         this.name = name;
       }
     
       get name() {
         return this._name;
       }
     
       set name(value) {
         if (value.length < 4) {
           alert("이름이 너무 짧습니다.");
           return;
         }
         this._name = value;
       }
     }
     
     let user = new User("보라");
     alert(user.name); // 보라
     
     user = new User(""); // 이름이 너무 짧습니다.
    
    • User.prototype에 정의됨

  • '클래스 필드'라는 문법을 사용해 여러 종류의 프로퍼티를 클래스에 추가할 수 있음
     class User {
       name = "보라"; // name 프로퍼티 추가 ('<프로퍼티>=<값>' 형식)
     
       sayHi() {
         alert(`${this.name}님 안녕하세요!`);
       }
     }
     
     new User().sayHi(); // 보라님 안녕하세요!
    
     let user = new User();
     alert(user.name); // 보라
     alert(User.prototype.name); // undefined
    
    • User.prototype이 아닌 개별 객체에만 클래스 필드가 설정됨

  • this의 컨텍스트를 알 수 없게 되는 '잃어버린 this'가 발생할 때
     class Button {
       constructor(value) {
         this.value = value;
       }
     
       click() {
         alert(this.value);
       }
     }
     
     let button = new Button("안녕하세요.");
     
     setTimeout(button.click, 1000); // undefined
     
    
    1. setTimeout(() => button.click(), 1000) 같이 래퍼 함수 전달
    2. 생성자 내부 등에서 메서드를 객체에 바인딩하기(함수 바인딩)
    3. 클래스 필드 사용 ex) 화살표 함수
      class Button {
        constructor(value) {
          this.value = value;
        }
        click = () => { // 화살표 함수는 자신의 `this`를 가지고 있지 않기 때문에 상위 스코프인 'Button' 클래스의 인스턴스를 참조
          alert(this.value);
        }
      }
      
      let button = new Button("안녕하세요.");	
      setTimeout(button.click, 1000); // 안녕하세요.
      

클래스 상속

  • extends 키워드를 사용해 클래스를 상속함
     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} 이/가 멈췄습니다.`);
       }
     }
     
     class Rabbit extends Animal { // Animal을 상속 받는 Rabbit 클래스
       hide() {
         alert(`${this.name} 이/가 숨었습니다!`);
       }
     }
     
     let rabbit = new Rabbit("흰 토끼");
     
     rabbit.run(5); // 흰 토끼 은/는 속도 5로 달립니다. (Animal 메서드 접근 가능)
     rabbit.hide(); // 흰 토끼 이/가 숨었습니다!
    

[!NOTE] extends 뒤에 표현식이 올 수도 있습니다.

  • extends 뒤에 표현식이 와도 괜찮음
  • extends 뒤에서 부모 클래스를 만들어주는 함수를 호출할 수 있음
function f(phrase) {
  return class {
  	sayHi() { alert(phrase) }
  }
}
class User extends f('Hello') {}
new User().sayHi(); // Hello

  • super 키워드를 사용해 부모 메서드의 기능을 변경 가능(오버라이딩)
    • super.method(...) 는 부모 클래스에 정의된 메서드로 method를 호출함
    • super(...)는 부모 생성자를 호출하고, 자식 생성자 내부에서만 사용할 수 있음
     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}가 멈췄습니다.`);
       }
     }
     
     class Rabbit extends Animal {
       hide() {
         alert(`${this.name}가 숨었습니다!`);
       }
     
       stop() {
         super.stop(); // 부모 클래스의 stop을 호출해 멈추고,
         this.hide(); // Rabbit의 hide 메서드 호출
       }
     }
     
     let rabbit = new Rabbit("흰 토끼");
     
     rabbit.run(5); // 흰 토끼가 속도 5로 달립니다.
     rabbit.stop(); // 흰 토끼가 멈췄습니다. 흰 토끼가 숨었습니다!
    

[!NOTE] 화살표 함수엔 super가 없습니다.

  • 화살표 함수 다시 살펴보기에서 살펴본 바와 같이, 화살표 함수는 super를 지원하지 않음
  • super에 접근하면 아래 예시와 같이 super를 외부 함수(상위 스코프)에서 가져옴
    class Rabbit extends Animal {
    	stop() {
    		setTimeout(()=> super.stop(), 1000); // 1초 후에 부모 stop을 호출
    	}
    }
    
    • 화살표 함수의 superstop()super와 같아서 위 예시는 의도한 대로 동작함
  • 아래의 코드처럼 setTimeout 내부에서 '일반' 함수를 사용하면 에러가 발생
    // Unexpected super
    setTimeout(function() { super.stop() }, 1000); 
    

  • 생성자는 기본적으로 부모 constructor를 호출함. 이때 부모 constructor에도 인수를 모두 전달함.
     class Animal {
       constructor(name) {
         this.speed = 0;
         this.name = name;
       }
       // ...
     }
     
     class Rabbit extends Animal {
       constructor(name, earLength) {
         this.speed = 0;
         this.name = name;
         this.earLength = earLength;
       }
       // ...
     }
     
     // 동작하지 않음
     let rabbit = new Rabbit("흰 토끼", 10); // ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
    
    • 상속 클래스의 생성자에선 반드시 super(...)를 호출해야 하는데, super(...)를 호출하지 않아 에러 발생. super(...)this를 사용하기 전에 반드시 호출해야 함.

  • super를 호출하는 이유
    • '상속 클래스의 생성자 함수와 그렇지 않은 생성자 함수'를 구분하는데 상속 클래스의 생성자 함수엔 특수 내부 프로퍼티인 [[ConstructorKind]]: 'derived'가 존재
  • 일반 클래스의 생성자 함수와 상속 클래스의 생성자 함수 간 차이는 new에서 드러남
    • 일반 클래스 - new와 함께 실행되면, 스스로 빈 객체가 만들어지고 this를 생성하고 초기화
    • 상속 클래스 - this를 초기화 하는 역할이 부모 클래스의 생성자에게 있음 > super를 이용해 부모 클래스의 생성자 함수를 호출해 초기화 진행
     class Animal {
       constructor(name) {
         this.speed = 0;
         this.name = name;
       }
       // ...
     }
     
     class Rabbit extends Animal {
       constructor(name, earLength) {
         super(name);
         this.earLength = earLength;
       }
       // ...
     }
     
     // 이제 에러 없이 동작합니다.
     let rabbit = new Rabbit("흰 토끼", 10);
     alert(rabbit.name); // 흰 토끼
     alert(rabbit.earLength); // 10
    

정적 메서드와 정적 프로퍼티

  • 클래스 함수 자체에 메서드를 설정할 수 있음 > 정적(static) 메서드
     // type1: 클래스 내부에 선언
     class User {
       static staticMethod() {
         alert(this === User);
       }
     }
     User.staticMethod(); // true
     
     // type2: 프로퍼티 형태로 직접 할당
     class User2 { }
     User2.staticMethod = function() {
       alert(this === User2);
     };
     User2.staticMethod(); // true
    
    • 이때 this의 값은 클래스 생성자인 User 자체가 됨(점 앞 객체)

  • 정적 메서드는 클래스에 속한 함수를 구현하고자 할 때 주로 사용됨
  • 개별 인스턴스가 아닌 클래스 자체에서 판단
     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
    
    • Article.compare이 정적 메서드가 아니었으면 articles 비교 불가능함

  • 정적 프로퍼티도 생성 가능
     // type1: 클래스 내부에 선언
     class Article {
       static publisher = "Ilya Kantor";
     }
     
     alert(Article.publisher); // Ilya Kantor
    
     // type2: 프로퍼티 형태로 직접 할당
     Article.publisher2 = "Ilya Kantor";
    

  • 정적 프로퍼티와 메서드는 상속됨
     class Animal {
       static planet = "지구";
     
       constructor(name, speed) {
         this.speed = speed;
         this.name = name;
       }
     
       run(speed = 0) {
         this.speed += speed;
         alert(`${this.name}가 속도 ${this.speed}로 달립니다.`);
       }
     
       static compare(animalA, animalB) {
         return animalA.speed - animalB.speed;
       }
     }
     
     // Animal을 상속받음
     class Rabbit extends Animal {
       hide() {
         alert(`${this.name}가 숨었습니다!`);
       }
     }
     
     let rabbits = [
       new Rabbit("흰 토끼", 10),
       new Rabbit("검은 토끼", 5)
     ];
     
     rabbits.sort(Rabbit.compare); // Animal에서 상속받은 정적 메서드를 사용
     rabbits[0].run(); // 검은 토끼가 속도 5로 달립니다.
     
     alert(Rabbit.planet); // 지구 (Animal에서 상속받은 정적 프로퍼티를 사용)
    

private, protected 프로퍼티와 메서드

  • 프로퍼티와 메서드의 분류
    • 내부 인터페이스 - 동일한 클래스 내의 다른 메서드에선 접근할 수 있지만, 외부에서 접근 불가능
      • private - 클래스 내부에서만 접근할 수 있으면 내부 인터페이스를 구성할 때 사용
      • protected - 자손 클래스에서 접근 가능 (private 보다 더 광범위하게 사용됨)
    • 외부 인터페이스 - 외부에서도 접근 가능
      • public - 어디서든지 접근 가능해 외부 인터페이스를 구성

  • protected 프로퍼티로 선언해 값이 수정되지 않게 프로퍼티를 보호할 수 있음
    • protected 프로퍼티 명 앞엔 밑줄(_)이 붙음
     class CoffeeMachine {
       _waterAmount = 0;
       
       // get과 set은 _waterAmount 에 대한 접근 방법을 제공
       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: 물의 양은 음수가 될 수 없습니다.
    

  • 읽기 전용 프로퍼티로 활용 가능
    • 생성할 때만 값을 할당하고, 그 이후에는 값을 절대 수정하지 못 함
    • 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 없음)
    

  • private 프로퍼티나 메서드 코드 예시
    • #으로 시작하면 private 프로퍼티와 메서드임
     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
    
  • private 필드는 public 필드와 충돌하지 않음
    • private 프로퍼티 #waterAmount와 public 프로퍼티 waterAmount를 동시에 가질 수 있음
    • 제약이 많아서 protected를 더 자주 사용
     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
    
     // 상속을 통해 접근 불가능
     class MegaCoffeeMachine exteds CoffeeMachine {
     	method() {
     		alert(this.#waterAmount); // Error: CoffeeMachine을 통해서만 접근 가능
     	}
     }
    

내장 클래스 확장하기

  • 배열, 맵 같은 내장 클래스도 확장 가능
     // 메서드 하나를 추가(더 많이 추가하는 것도 가능).
     class PowerArray extends Array {
       isEmpty() {
         return this.length === 0;
       }
     }
     
     let arr = new PowerArray(1, 2, 5, 10, 50);
     alert(arr.isEmpty()); // false
     
     let filteredArr = arr.filter(item => item >= 10);
     alert(filteredArr); // 10, 50
     alert(filteredArr.isEmpty()); // false
    

  • 특수 정적 getter인 Symbol.species를 클래스에 추가하면 map, filter 등의 메서드를 호출할 때 만들어지는 개체의 생성자를 지정할 수 있음
     class PowerArray extends Array {
       isEmpty() {
         return this.length === 0;
       }
     
       // 내장 메서드는 반환 값에 명시된 클래스를 생성자로 사용
       static get [Symbol.species]() {
         return Array; // 일반 배열 반환
       }
     }
     
     let arr = new PowerArray(1, 2, 5, 10, 50);
     alert(arr.isEmpty()); // false
     
     // filter는 메서드는 PowerArray가 아닌 Array를 생성자로 사용하여 새로운 배열을 만듦
     let filteredArr = arr.filter(item => item >= 10);
     
     // filteredArr는 PowerArray가 아닌 Array의 인스턴스
     alert(filteredArr.isEmpty()); // Error: filteredArr.isEmpty is not a function
    

[!info] 다른 컬렉션도 유사하게 동작합니다. Map, Set 같은 컬렉션도 위와 같이 동작하고, Symbol.species를 사용함


  • 내장 객체는 Object.keys, Array.isArray 등의 자체 정적 메서드를 가짐
  • 일반적으로 한 클래스가 다른 클래스를 상속받는 경우 정적 메서드와 일반 메서드 모두 상속받음
  • 내장 클래스는 정적 메서드를 상속 받지 못함
    • 정적 메서드/프로퍼티는 prototype에 정의된 것이 아니라 클래스 자체에 속하는 메서드/프로퍼티이기 때문에 프로토타입 체인을 통해 상속되지 않음

'instanceof'로 클래스 확장하기

  • instanceof를 사용하면 객체가 특정 클래스에 속하는지 아닌지를 확인할 수 있음 + 상속 관계도 확인 가능
// 클래스
class Rabbit {}
let rabbit = new Rabbit();

// rabbit이 클래스 Rabbit의 객체인지 확인
alert(rabbit instanceof Rabbit); // true
// rabbit.__proto__ === Rabbit.prototype // true

// 생성자 함수
function Rabbit2() {}

alert(new Rabbit2() instanceof Rabbit2); // true

// 내장 클래스
let arr = [1, 2, 3];
alert(arr instanceof Array); // true
alert(arr instanceof Object); // true

  • obj instanceof Class 동작 과정
    1. 클래스에 정적 메서드 Symbol.hasInstance가 구현되어 있으면, obj instanceof Class 문이 실행될 때, Class[Symbol.hasInstance](obj)가 호출됨
      • truefalse로 반환
      • 커스터마이징 가능
     // 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. 대부분의 클래스엔 Symbol.hasInstance가 구현되어 있지 않음. 일반적인 로직이 사용됨. obj instanceof ClassClass.prototypeobj 프로토타입 중 하나와 일치하는지 확인
      obj.__proto__ === Class.prototype?
      obj.__proto__.__proto__ === Class.prototype? // 상속의 경우
      obj.__proto__.__proto__.__proto__ === Class.prototype?
      ...
      // 이 중 하나라도 true라면 true를 반환
      // 그렇지 않고 체인의 끝에 도달하면 false를 반환
      

  • obj instanceof Class와 동일한 기능인 Class.prototype.isPrototypeOf(obj)가 존재 (objA.isPrototypeOf(objB))
    • Class 생성자를 제외하고 포함 여부를 검사
    • 검사 시, 프로토타입 체인과 Class.prototype만 고려
    • 프로토타입이 변경되면 결과도 달라짐
     function Rabbit() {}
     let rabbit = new Rabbit();
     
     // 프로토타입이 변경됨
     Rabbit.prototype = {};
     
     // 더 이상 Rabbit이 아닙니다!
     alert(rabbit instanceof Rabbit); // false
    

  • 객체에 내장 toString을 추출하는 게 가능함
  • 모든 값을 대상으로 실행할 수 있고, 호출 결과는 값에 따라 달라짐
    • 숫자형 – [object Number]
    • 불린형 – [object Boolean]
    • null – [object Null]
    • undefined – [object Undefined]
    • 배열 – [object Array]
    • 그외 – 커스터마이징 가능
     // 편의를 위해 toString 메서드를 변수에 복사함
     let objectToString = Object.prototype.toString;
     
     // 아래 변수의 타입은?
     let arr = [];
     alert(objectToString.call(arr)); // [object Array]
    
     let s = Object.prototype.toString;
     
     alert(s.call(123)); // [object Number]
     alert(s.call(null)); // [object Null]
     alert(s.call(alert)); // [object Function]
    

  • 특수 객체 프로퍼티 Symbol.toStringTag를 사용하면 toString의 동작을 커스터마이징할 수 있음
     let user = {
       [Symbol.toStringTag]: "User"
     };
     
     alert({}.toString.call(user)); // [object User]
    

믹스인

  • 자바스크립트는 단일 상속만 허용
    • 객체엔 단 하나의 [[Prototype]]만 있을 수 있고, 클래스는 크래스 하나만 상속받을 수 있음
  • 이런 제약이 한계가 느껴질 때 믹스인(mixin)을 사용
    • 다른 클래스를 상속받을 필요 없이 이들 클래스에 구현되어 있는 메서드를 담고 있는 클래스라고 정의함
    • 특정 행동을 실행해주는 메서드를 제공하는데 단독으로 쓰이지 않고 다른 클래스에 행동을 더해주는 용도로 사용

  • 유용한 메서드 여러 개가 담긴 객체를 하나를 믹스인으로 구현
     // 믹스인
     let sayHiMixin = {
       sayHi() {
         alert(`Hello ${this.name}`);
       },
       sayBye() {
         alert(`Bye ${this.name}`);
       }
     };
     
     // 사용법:
     class User {
       constructor(name) {
         this.name = name;
       }
     }
     
     // 상속 없이 메서드만 복사
     Object.assign(User.prototype, sayHiMixin);
     
     // 이제 User가 인사를 할 수 있습니다.
     new User("Dude").sayHi(); // Hello Dude!
    
  • 다른 클래스를 상속 받는 동시에, 믹스인에 구현된 추가 메서드도 사용할 수 있음
     class User extends Person {
       // ...
     }
     
     Object.assign(User.prototype, sayHiMixin);
    
  • 믹스인 내부에 믹스인 상속도 가능
     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!
    
    • (*) - sayHiMixin에서 부모 메서드 super.say()를 호출하면 클래스가 아닌 sayHiMixin의 프로토타입 메서드를 찾음
      • sayHisayBye가 생성된 곳이 sayHiMixin이기 때문

  • 클래스나 객체에 이벤트 관련 함수를 쉽게 추가할 수 있도록 해주는 믹스인을 만들 수 있음
    • .trigger(name, [...data] - 특정 이벤트(name)를 발생시킴
      • name - 이벤트 이름 / data - 이벤트와 함께 전달될 추가 정보
    • .on(name, handler) - 특정 이벤트(name)에 대한 리스너(handler)를 등록
      • 이벤트가 트리거될 때 등록된 리스너가 호출되며, 트리거 시 전달될 인수(data)를 받음
    • off(name, handler) - 특정 이벤트(name)에 대한 리스너(handler)를 제거함
     let eventMixin = {
       /**
        *  이벤트 구독 (이벤트 리스너 등록)
        *  사용패턴: 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);
       },
     
       /**
        *  구독 취소 (이벤트 리스너 제거)
        *  사용패턴: 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));
       }
     };
     
     // 클래스 생성
     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");
    

  • 믹스인으로 구현하면 위의 코드처럼 User 외에도 다양한 클래스에서 공용으로 사용 가능