2022年8月27日

ミックスイン

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);

ミックスインは、それら自体の中に継承を使用できます。

たとえば、ここではsayHiMixinsayMixinから継承しています。

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では多重継承はサポートされていませんが、メソッドをプロトタイプにコピーすることでミキシンを実装できます。

上記で見たように、イベント処理など、複数の動作を追加してクラスを拡張する方法としてミキシンを使用できます。

ミキシンは、誤って既存のクラスメソッドを上書きすると競合点になる可能性があります。そのため、一般的にはミキシンのネーミングメソッドを十分に検討して発生する可能性を最小限に抑える必要があります。

チュートリアルマップ

コメント

コメントする前にこれをお読みください…
  • 改善すべき点がある場合は、コメントする代わりにGitHubの問題またはプルリクエストを送信してください。
  • 記事内の内容が理解できない場合 - 詳細を教えてください。
  • コードの фрагмент を挿入するには <code> タグを使用し、複数の行は <pre> タグで囲み、10 行以上は sandbox(plnkrjsbincodepen…)を使用します。