JavaScriptでは1つのオブジェクトからしか継承できない。オブジェクトには1つの[[Prototype]]
しか設定できない。そして、クラスは1つのクラスのみを継承できる。
しかし、時にはこれが制限に感じることもある。たとえば、StreetSweeper
クラスとBicycle
クラスがあり、それらをミックスしてStreetSweepingBicycle
を作成したいとする。
または、User
クラスとイベントの生成を実装するEventEmitter
クラスがあり、EventEmitter
の機能をUser
に追加して、ユーザーがイベントを発行できるようにしたいとする。
ここには「ミックスイン」と呼ばれる概念が役立つ。
Wikipediaによれば、ミックスインとは、それ自体から継承することなく、他のクラスで使用できるメソッドを含むクラスである。
つまり、ミックスインは特定の動作を実装するメソッドを提供しますが、それ自体では使用せず、他のクラスに動作を追加するために使用します。
ミックスインの例
JavaScriptでミックスインを実装するための最も簡単な方法は、便利なメソッドを含むオブジェクトを作成し、任意のクラスのプロトタイプに簡単にマージできるようにすることです。
たとえば、次は、ミックスインsayHiMixin
を使用して、User
にいくつかの「会話」を追加します
// mixin
let sayHiMixin = {
sayHi() {
alert(`Hello ${this.name}`);
},
sayBye() {
alert(`Bye ${this.name}`);
}
};
// usage:
class User {
constructor(name) {
this.name = name;
}
}
// copy the methods
Object.assign(User.prototype, sayHiMixin);
// now User can say hi
new User("Dude").sayHi(); // Hello Dude!
継承はありませんが、単純なメソッドのコピーがあります。したがって、User
は別のクラスから継承し、このように追加のメソッドを「ミックスイン」するためにミックスインを含めることができます
class User extends Person {
// ...
}
Object.assign(User.prototype, sayHiMixin);
ミックスインは、それら自体の中に継承を使用できます。
たとえば、ここではsayHiMixin
はsayMixin
から継承しています。
let sayMixin = {
say(phrase) {
alert(phrase);
}
};
let sayHiMixin = {
__proto__: sayMixin, // (or we could use Object.setPrototypeOf to set the prototype here)
sayHi() {
// call parent method
super.say(`Hello ${this.name}`); // (*)
},
sayBye() {
super.say(`Bye ${this.name}`); // (*)
}
};
class User {
constructor(name) {
this.name = name;
}
}
// copy the methods
Object.assign(User.prototype, sayHiMixin);
// now User can say hi
new User("Dude").sayHi(); // Hello Dude!
sayHiMixin
から親メソッドsuper.say()
を呼び出すと ((*)
でラベル付けされた行)、クラスではなくそのミックスインのプロトタイプにあるメソッドを検索することに注意してください。
図を示します(右側を参照してください)
その理由は、sayHi
メソッドとsayBye
メソッドが最初にsayHiMixin
で作成されたからです。コピーされたとしても、sayHiMixin
によって内部プロパティである[[HomeObject]]
は参照されるため、上の図のようになります。
super
は[[HomeObject]].[[Prototype]]
の親メソッドを検索するので、sayHiMixin.[[Prototype]]
が検索されます。
EventMixin
実際の生活でミキシンを作ってみましょう。
多くのブラウザオブジェクトの重要な機能(たとえば)は、イベントを生成できることです。イベントは、それを望む人に「情報をブロードキャスト」するための優れた方法です。そこで、イベント関連の関数をあらゆるクラスまたはオブジェクトに簡単に追加できるミキシンを作ります。
- ミキシンは
.trigger(name, [...data])
メソッドを提供して、重要なことが起こったときに「イベントを生成」します。name
引数はイベントの名前で、省略可能ですが、イベントデータを含む追加の引数が続きます。 - また、
.on(name, handler)
メソッドは、特定の名前を持つイベントのリスナーとしてhandler
関数を追加します。指定されたname
を持つイベントがトリガーされたときに呼び出され、.trigger
コールからの引数を受け取ります。 - …そして、
handler
リスナーを削除する.off(name, handler)
メソッドがあります。
ミキシンを追加すると、訪問者がログインすると、user
オブジェクトは"login"
イベントを生成できます。そして、別のオブジェクト(たとえばcalendar
)は、ログインしたユーザー用にカレンダーをロードするために、そのようなイベントをリッスンする可能性があります。
または、menu
はメニュー項目が選択されたときに"select"
イベントを生成し、他のオブジェクトはそのイベントに対してリアクションするハンドラーを割り当てることができます。そして、他にもあります。
コードは次のとおりです。
let eventMixin = {
/**
* Subscribe to event, usage:
* 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);
},
/**
* Cancel the subscription, usage:
* 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);
}
}
},
/**
* Generate an event with the given name and data
* this.trigger('select', data1, data2);
*/
trigger(eventName, ...args) {
if (!this._eventHandlers?.[eventName]) {
return; // no handlers for that event name
}
// call the handlers
this._eventHandlers[eventName].forEach(handler => handler.apply(this, args));
}
};
.on(eventName, handler)
- 指定された名前のイベントが発生したときに実行される関数handler
を割り当てます。技術的には、イベント名ごとにハンドラの配列を格納する_eventHandlers
プロパティがあり、リストに追加されるだけです。.off(eventName, handler)
- 関数をハンドラのリストから削除します。.trigger(eventName, ...args)
- イベントを生成します。_eventHandlers[eventName]
からのすべてのハンドラが、引数のリスト...args
を使用して呼び出されます。
使用法
// Make a class
class Menu {
choose(value) {
this.trigger("select", value);
}
}
// Add the mixin with event-related methods
Object.assign(Menu.prototype, eventMixin);
let menu = new Menu();
// add a handler, to be called on selection:
menu.on("select", value => alert(`Value selected: ${value}`));
// triggers the event => the handler above runs and shows:
// Value selected: 123
menu.choose("123");
ここで、メニューの選択に反応するコードが必要な場合は、menu.on(...)
を使用してリッスンできます。
そして、eventMixin
ミキシンがあると、そのような動作を好きなだけのクラスに簡単に追加できます。継承チェーンを妨げることはありません。
まとめ
Mixin - オブジェクト指向プログラミングの一般的な用語で、他のクラスのメソッドを含むクラスです。
他の言語では多重継承が許可されています。JavaScriptでは多重継承はサポートされていませんが、メソッドをプロトタイプにコピーすることでミキシンを実装できます。
上記で見たように、イベント処理など、複数の動作を追加してクラスを拡張する方法としてミキシンを使用できます。
ミキシンは、誤って既存のクラスメソッドを上書きすると競合点になる可能性があります。そのため、一般的にはミキシンのネーミングメソッドを十分に検討して発生する可能性を最小限に抑える必要があります。
コメント
<code>
タグを使用し、複数の行は<pre>
タグで囲み、10 行以上は sandbox(plnkr、jsbin、codepen…)を使用します。