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
User
이라는 이름을 가진 함수를 생성. 이때 생성자 메서드constructor
에서 가져옴. 생성자 메서드가 없으면 본문이 비워진 채로 함수가 만들어짐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();
- 생성자 함수나 클래스로 만든 함수나 결과는 거의 같지만 몇몇 차이점이 존재
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
가 사용됨
- 클래스 생성자를
- 클래스에 정의된 메서드는 열거할 수 없음(non-enumerable). 클래스의
prototype
프로퍼티에 추가된 메서드의enumerable
플래그는false
- 클래스는 항상
엄격 모드
로 실행됨(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
setTimeout(() => button.click(), 1000)
같이 래퍼 함수 전달- 생성자 내부 등에서 메서드를 객체에 바인딩하기(함수 바인딩)
- 클래스 필드 사용 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을 호출 } }
- 화살표 함수의
super
는stop()
의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 보다 더 광범위하게 사용됨)
- 외부 인터페이스 - 외부에서도 접근 가능
- 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: 물의 양은 음수가 될 수 없습니다.
- protected 프로퍼티 명 앞엔 밑줄(
- 읽기 전용 프로퍼티로 활용 가능
- 생성할 때만 값을 할당하고, 그 이후에는 값을 절대 수정하지 못 함
- 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을 통해서만 접근 가능 } }
- private 프로퍼티
- 배열, 맵 같은 내장 클래스도 확장 가능
// 메서드 하나를 추가(더 많이 추가하는 것도 가능). 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
를 사용하면 객체가 특정 클래스에 속하는지 아닌지를 확인할 수 있음 + 상속 관계도 확인 가능
// 클래스
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
동작 과정- 클래스에 정적 메서드
Symbol.hasInstance
가 구현되어 있으면,obj instanceof Class
문이 실행될 때,Class[Symbol.hasInstance](obj)
가 호출됨true
나false
로 반환- 커스터마이징 가능
// 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)가 호출됨
- 대부분의 클래스엔
Symbol.hasInstance
가 구현되어 있지 않음. 일반적인 로직이 사용됨.obj instanceof Class
는Class.prototype
이obj
프로토타입 중 하나와 일치하는지 확인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
의 프로토타입 메서드를 찾음sayHi
와sayBye
가 생성된 곳이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
외에도 다양한 클래스에서 공용으로 사용 가능