サイトトップ

Director Flash 書籍 業務内容 プロフィール

HTML5テクニカルノート

関数に任意のthis参照を定める

ID: FN1303002 Technique: HTML5 and JavaScript

JavaScriptで関数を定めたとき、関数本体のthis参照はその関数がどのオブジェクトに与えられたかによって変わります。CreateJSのライブラリSoundJS 0.4.0には、関数のthis参照先を定めるproxy()メソッドが備わりました。このメソッドを題材に、関数にthis参照を定めるやり方について考えます。


01 イベントハンドラとイベントリスナーのthis参照

this参照のありかがよく問われるのは、イベントで呼出すハンドラやリスナーの関数です。まず、原則を確かめましょう。HTMLドキュメントのbody要素に、以下のようにinput要素でボタン(type属性"button")を加えます(図001)。input要素にはJavaScriptで参照が得られるように、id属性に識別子("control")を定めておきます。

図001■HTMLドキュメントのbody要素にinput要素でボタンを置いた
図001

<body onLoad="initialize()">
  <input type="button" id="control" value="button" />
</body>

script要素では、document.getElementById()メソッドで得たinput要素のボタンに、onclickハンドラ(showThisReference())を定めます(第3〜4行目)。ハンドラの関数は、window.alert()メソッドで警告ダイアログボックスにthis参照を示します(第7行目)。

  1. <script>
  2. function initialize() {
  3.   var instance = document.getElementById("control");
  4.   instance.onclick = showThisReference;
  5. }
  6. function showThisReference() {
  7.   alert(this);
  8. }
  9. </script>
  10. </head>

JavaScriptでは、関数はFunctionというオブジェクトです。そして、関数をオブジェクトに定めたハンドラは、プロパティとして扱われます。そのため、ハンドラの関数本体では、thisキーワードはハンドラが定められたオブジェクトを参照します。したがって、上述のJavaScriptコードでボタンをクリックすると、警告ダイアログボックスにはthis参照としてつぎのようにinput要素が示されます(図007)。

[object HTMLInputElement]

図002■警告ダイアログボックスにinput要素が示された
図002

CreateJS(EaselJS 0.6.0)では、イベントは基本的にイベントリスナーで扱います。そのリスナー関数におけるthis参照を確かめましょう。body要素にはcanvas要素が加えられ、id属性に識別子"myCanvas"が定められているとします。

<body onLoad="initialize()">
  <canvas id="myCanvas" width="240" height="180"></canvas>
</body>

以下のJavaScriptコードは、Canvas左上角に正方形の青いShapeインスタンスを置きます(図003)。そして、そのオブジェクトがクリックされたときのDisplayObject.clickイベントに、EventDispatcher.addEventListener()メソッドでリスナー関数(showThisReference())を加えました(第9行目)。この関数は、window.alert()メソッドで警告ダイアログボックスにthis参照を示します(第13行目)。

図003■Canvasの左上角に青い正方形のShapeオブジェクトが置かれた
図003

  1. <script src="http://code.createjs.com/easeljs-0.6.0.min.js"></script>
  2. <script>
  3. var stage;
  4. function initialize() {
  5.   var canvasElement = document.getElementById("myCanvas");
  6.   var instance = createRectangle(0, 0, 20, 20);
  7.   stage = new createjs.Stage(canvasElement);
  8.   stage.addChild(instance);
  9.   instance.addEventListener("click", showThisReference);
  10.   stage.update();
  11. }
  12. function showThisReference() {
  13.   alert(this);
  14. }
  15. function createRectangle(x, y, width, height) {
  16.   var myShape = new createjs.Shape();
  17.   var myGraphics = myShape.graphics;
  18.   myGraphics.beginFill("blue");
  19.   myGraphics.drawRect(x, y, width, height);
  20.   return myShape;
  21. }
  22. </script>

EaselJSのリスナー関数は、イベントハンドラとは異なり、イベントが起こったオブジェクトではなくグローバルなオブジェクトのもとで呼出されます(「EaselJSでイベントリスナーを扱うEventDispatcherクラス」)。そのため、Shapeインスタンスをクリックすると、警告ダイアログボックスにはthis参照としてWindowオブジェクトが示されます(図004)。

[object DOMWindow]

図004■警告ダイアログボックスにWindowオブジェクトが示された
図004


02 SoundJSライブラリでproxy()メソッドを使う

CreateJSのSoundJS 0.4.0ライブラリには、関数におけるthis参照を定められる静的メソッドproxy()が備わりました。厳密には、メソッドはクラスには属さず、グローバルな関数の扱いです。ただし、Soundクラスを読込まなければなりません。proxy()メソッドには、関数とその中でthis参照に定めたいオブジェクトを引数に渡します。メソッドの戻り値がthis参照を定めた関数です。

createjs.proxy(関数, スコープ)

メソッドのリファレンス「SoundJSで関数にスコープを定める ー proxy()メソッド」には、以下のような例を掲げました(再掲FN1303001コード001)。外部ファイルから読込んだMP3サウンドを、ボタンのクリックにより停止し、さらに頭から再生し直します。本稿初めにご紹介したコードと同じく、input要素で加えたボタン(id属性"control")ひとつを、切替えて用います。

静的メソッドSound.play()から返されたSoundInstanceオブジェクト(instance)のSoundInstance.completeイベントにリスナー関数を加えました(第7〜8行目)。リスナーはproxy()メソッドの戻り値とし、呼出す関数(soundCompleted())にはスコープとしてボタンのインスタンス(control)を渡しました。

proxy()メソッドの第1引数に渡した関数(soundCompleted)は、イベントオブジェクトのtargetプロパティからSoundInstanceオブジェクト、スコープに定めたthis参照からボタンのオブジェクトを得て、これらふたつを別の関数(readyToPlay)に引数として渡して呼出します(第12〜13行目)。 呼出された関数(readyToPlay)は、受取った引数のボタンに定めるonclickハンドラを切替え、クリックしたらSoundInstance.play()メソッドによりサウンドを頭から再生し直します(第16〜17行目)。その他のスクリプトの中身について詳しくは、前出リファレンスの解説をお読みください。

FN1303001コード001■ボタンクリックでサウンドを停止・再生する(再掲)
  1. <script src="http://code.createjs.com/soundjs-0.4.0.min.js"></script>
  2. <script>
  3. createjs.Sound.addEventListener("loadComplete", loadHandler);
  4. createjs.Sound.registerSound("sounds/test.mp3", "sound");
  5. function loadHandler(eventObject) {
  6.   var instance = createjs.Sound.play("sound");
  7.   var control = document.getElementById("control");
  8.   instance.addEventListener("complete", createjs.proxy(soundCompleted, control));
  9.   readyToStop(instance, control);
  10. }
  11. function soundCompleted(eventObject) {
  12.   var instance = eventObject.target;
  13.   readyToPlay(instance, this);
  14. }
  15. function readyToPlay(instance, control) {
  16.   control.onclick = function () {
  17.     instance.play();
  18.     readyToStop(instance, control);
  19.   };
  20.   control.value = "play";
  21. }
  22. function readyToStop(instance, control) {
  23.   control.onclick = function () {
  24.     instance.stop();
  25.     readyToPlay(instance, control);
  26.   };
  27.   control.value = "stop";
  28. }
  29. </script>

proxy()メソッドでボタンの参照をスコープに定めたため、グローバルな変数を用いることなく、SoundInstance.completeイベントのリスナー関数(soundCompleted)にボタンがthis参照として渡せました。


03 EaselJSライブラリのマウスイベントにおけるリスナーの参照

CreateJSのEaselJSライブラリでは、マウスイベントのリスナーをどのオブジェクトが扱うのかに注意しなければなりません。クリックやマウスボタンを押すイベントは、それぞれDisplayObject.clickDisplayObject.mousedownなのでマウス操作の対象(DisplayObjectインスタンス)にリスナーを加えます。ところが、マウスを動かしたり、ボタンを放す操作は、MouseEventオブジェクトのMouseEvent.mousemoveMouseEvent.mouseupイベントで扱います。

前述01「イベントハンドラとイベントリスナーのthis参照」でイベントリスナーを試したJavaScriptコードに手直しします。Shapeインスタンスの上でマウスボタンを押すイベントDisplayObject.mousedownで、MouseEvent.mouseupのリスナー(release())を加えます。そして、そのリスナー関数の本体で、イベントリスナーは除くことにします。

問題は、MouseEvent.mouseupイベントのリスナーが加えられたMouseEventオブジェクトの参照を、リスナー関数(release())からどうやって捉えるかです。リスナー関数の受取った引数は、MouseEvent.mouseupイベントのオブジェクトですので、リスナーを登録した(DisplayObject.mousedownイベントの)オブジェクトではありません。

MouseEvent.mouseupイベントのリスナーから、DisplayObject.mousedownのイベントオブジェクトを参照できるように考えたのが、以下のコード001です。DisplayObject.mousedownイベントのリスナー関数(press())は、引数に受取ったMouseEventオブジェクト(eventObject)のtargetプロパティからマウスで操作したインスタンス(instance)を取出し、そのプロパティ(dispatcher)としてイベントオブジェクトを与えました(第11〜12行目)。

MouseEvent.mouseupイベントのリスナーも、引数のMouseEventオブジェクト(eventObject)からtargetプロパティでマウスで操作したインスタンス(instance)が得られます(第16行目)。すると、インスタンスのプロパティ(dispatcher)に与えたリスナーが登録されているイベントオブジェクトも取出せるのです(第17行目)。

これで、正しいMouseEventオブジェクト(dispatcher)を参照して、EventDispatcher.removeEventListener()メソッドが呼出せます(第20行目)。なお、イベントリスナーを削除する前後に、EventDispatcher.hasEventListener()メソッドでリスナーの有無を調べて、配列(result)に納めています(第19および第21行目)。その配列をwindow.alert()メソッドに渡すと(第22行目)、警告ダイアログボックスにはつぎのように示されますので、リスナーは正しく除かれたことが確かめられます(図005)。

true,false

図005■警告ダイアログボックスにEventDispatcher.hasEventListener()メソッドの戻り値が示される
図005

コード001■イベントリスナーをマウスボタンが押されたとき加えて放したら除く
  1. var stage;
  2. function initialize() {
  3.   var canvasElement = document.getElementById("myCanvas");
  4.   var instance = createRectangle(0, 0, 20, 20);
  5.   stage = new createjs.Stage(canvasElement);
  6.   stage.addChild(instance);
  7.   instance.addEventListener("mousedown", press);
  8.   stage.update();
  9. }
  10. function press(eventObject) {
  11.   var instance = eventObject.target;
  12.   instance.dispatcher = eventObject;
  13.   eventObject.addEventListener("mouseup", release);
  14. }
  15. function release(eventObject) {
  16.   var instance = eventObject.target;
  17.   var dispatcher = instance.dispatcher;
  18.   var result = [];
  19.   result.push(dispatcher.hasEventListener("mouseup"));
  20.   dispatcher.removeEventListener("mouseup", release);
  21.   result.push(dispatcher.hasEventListener("mouseup"));
  22.   alert(result);   // 表示: true, false
  23. }
  24. function createRectangle(x, y, width, height) {
  25.   var myShape = new createjs.Shape();
  26.   var myGraphics = myShape.graphics;
  27.   myGraphics.beginFill("blue");
  28.   myGraphics.drawRect(x, y, width, height);
  29.   return myShape;
  30. }

04 EaselJSライブラリでFunction.bind()メソッドを使う

proxy()メソッドは、SoundJSライブラリを読込まないと使えません。けれど、JavaScriptにはFunction.bind()という関数のthis参照を変えるメソッドがあります。このメソッドは関数に対して呼出し、this参照に定めたいオブジェクトを引数として渡します。

関数.bind(スコープ)

すると、マウスイベントのリスナーを削除する前掲コード001は、Function.bind()メソッドでつぎのように書替えられそうな気がするかもしれません。

function press(eventObject) {
  /*
  var instance = eventObject.target;
  instance.dispatcher = eventObject;
  eventObject.addEventListener("mouseup", release);
  */

  eventObject.addEventListener("mouseup", release.bind(eventObject));
}
function release(eventObject) {
  // var instance = eventObject.target;
  // var dispatcher = instance.dispatcher;

  var result = [];
  /*
  result.push(dispatcher.hasEventListener("mouseup"));
  dispatcher.removeEventListener("mouseup", release);
  result.push(dispatcher.hasEventListener("mouseup"));
  */

  result.push(this.hasEventListener("mouseup"));
  this.removeEventListener("mouseup", release);
  result.push(this.hasEventListener("mouseup"));
  alert(result);     // 表示: true, true
}

ところが、試してみるとイベントリスナーは消えません。しかし、EventDispatcher.hasEventListener()メソッドの戻り値がtrueなのですから、EventDispatcher.removeEventListener()メソッドで参照しているオブジェクトは確かにリスナーをもっており、間違っていないということです(図006)。

図006■イベントオブジェクトはリスナーをもっているのに消えない
図006

間違っているのはEventDispatcher.removeEventListener()メソッドの第2引数に渡したリスナー関数(release)です。Function.bind()メソッドが返すのは、参照した関数のスコープを第2引数のオブジェクトに置換えた新たな関数です。つまり、メソッドが参照した関数と戻り値の関数とは別物なのです。したがって、EventDispatcher.removeEventListener()メソッドの第2引数にFunction.bind()メソッドで参照した関数を渡しても、リスナーではないので除けません(詳しくは、「EaselJSのイベントリスナーにFunction.bind()メソッドを適用するとリスナー内から削除できない」 参照)。

ではどうしたらよいかといいますと、Function.bind()メソッドの戻り値を何らかのやり方でリスナー関数に渡さなければなりません。とはいえ、グローバルな変数やマウス操作をしたインスタンスのプロパティに定めるのでは、前掲コード001と代わり映えせず、Function.bind()メソッドを使う意味がありません。

ひとつ考えられるのは、リスナー関数の引数に加えることです。Function.bind()メソッドには第2引数以降を与えることができ、それらは戻り値の関数に引数として渡されます。

関数.bind(スコープ, 引数, …)

けれども、Function.bind()メソッドを呼出さなければ、戻り値の関数の参照が得られません。そして、呼出してしまったら、メソッドに引数が加えられないのです。そこで、前掲コード001を以下のように書替えてみました。script要素全体は、後にコード002として掲げます(行番号はコード002にもとづきます)。

MouseEvent.mouseupイベントのリスナーを定めるとき、Function.bind()メソッドの第2引数に空のObjectインスタンス(item)を渡し、戻り値の関数は予め変数(listener)にとりました(第11〜12行目)。そして、そのオブジェクトのプロパティ(listener)として、変数の参照を与えています(第13行目)。そのうえで、EventDispatcher.addEventListener()メソッドは、その変数を第2引数に渡して呼出しました(第14行目)。

これで、MouseEvent.mouseupイベントのリスナー関数(release())は、引数としてMouseEventオブジェクトのほかにもうひとつObjectインスタンスを受取ります。ただし、Function.bind()メソッドの第2引数以降に渡した値(item)は、参照した関数がもともと受取る引数(eventObject)の前に加わります(第16行目)。後は、第1引数のオブジェクトからプロパティ(listener)に与えられた関数の参照を取出し、EventDispatcher.removeEventListener()メソッドの第2引数に渡せば、リスナーが正しく除けます(第17および第20行目)。

  1. function press(eventObject) {
      /*
      var instance = eventObject.target;
      instance.dispatcher = eventObject;
      eventObject.addEventListener("mouseup", release);
      */
  2.   var item = {};
  3.   var listener = release.bind(eventObject, item);
  4.   item.listener = listener;
  5.   eventObject.addEventListener("mouseup", listener);
  6. }
    // function release(eventObject) {
  7. function release(item, eventObject) {
      // var instance = eventObject.target;
      // var dispatcher = instance.dispatcher;
  8.   var listener = item.listener;
  9.   var result = [];
      /*
      result.push(dispatcher.hasEventListener("mouseup"));
      dispatcher.removeEventListener("mouseup", release);
      result.push(dispatcher.hasEventListener("mouseup"));
      */
  10.   result.push(this.hasEventListener("mouseup"));
  11.   this.removeEventListener("mouseup", listener);
  12.   result.push(this.hasEventListener("mouseup"));
  13.   alert(result);   // 表示: true, false
  14. }

window.alert()メソッドで開く警告ダイアログボックスの値を確かめると、EventDispatcher.removeEventListener()メソッドを呼出した後EventDispatcher.hasEventListener()メソッドがfalseを返します(第22行目)。script要素に書いたJavaScript全体は、つぎのコード002のとおりです。

コード002■リスナー関数の引数にリスナー自身の参照を加えてイベントから削除した
  1. var stage;
  2. function initialize() {
  3.   var canvasElement = document.getElementById("myCanvas");
  4.   var instance = createRectangle(0, 0, 20, 20);
  5.   stage = new createjs.Stage(canvasElement);
  6.   stage.addChild(instance);
  7.   instance.addEventListener("mousedown", press);
  8.   stage.update();
  9. }
  10. function press(eventObject) {
  11.   var item = {};
  12.   var listener = release.bind(eventObject, item);
  13.   item.listener = listener;
  14.   eventObject.addEventListener("mouseup", listener);
  15. }
  16. function release(item, eventObject) {
  17.   var listener = item.listener;
  18.   var result = [];
  19.   result.push(this.hasEventListener("mouseup"));
  20.   this.removeEventListener("mouseup", listener);
  21.   result.push(this.hasEventListener("mouseup"));
  22.   alert(result);   // 表示: true, false
  23. }
  24. function createRectangle(x, y, width, height) {
  25.   var myShape = new createjs.Shape();
  26.   var myGraphics = myShape.graphics;
  27.   myGraphics.beginFill("blue");
  28.   myGraphics.drawRect(x, y, width, height);
  29.   return myShape;
  30. }

05 proxy()メソッドを定める

前掲コード002のFunction.bind()メソッドの使い方は、手軽さに欠けます。また、SoundJSライブラリのproxy()メソッドは、実装を見るとつぎのようにわずか6行です。それなら、SoundJS以外でも使いやすいように、proxy()メソッドをつくり直してもよいでしょう。

  1. createjs.proxy = function (method, scope) {
  2.   var aArgs = Array.prototype.slice.call(arguments, 2);
  3.   return function () {
  4.     return method.apply(scope, Array.prototype.slice.call(arguments, 0).concat(aArgs));
  5.   };
  6. }

proxy()メソッドの実装のJavaScriptコードについては、「SoundJSで関数にスコープを定める ー proxy()メソッド」の「実装」に解説しましたので、お読みください。ここでは、このメソッドをもとにして、戻り値の関数に引数として関数自身の参照を加えることにします。

SoundJSライブラリのproxy()メソッドも、Function.bind()メソッドと同じように、戻り値の関数に渡す引数が与えられます。これから定める自作proxy()関数は、戻り値の関数自身への参照を引数の最後に内部的に加えるようにします。

proxy(関数, スコープ, 引数, …, 関数自身への参照)

つくり直したproxy()関数は、つぎのとおりです。前掲コード002を新たなproxy()関数で書替えたscript要素全体は、後にコード003として掲げます(行番号はコード003にもとづきます)。

proxy()関数が第3引数以降に受取って、戻り値の関数に渡す値は、配列にして変数(aArgs)に納められます(第29行目)。その引数に加えるため、戻り値にする関数は変数(myFunction)に定めます。その関数の中で、引数の配列にArray.push()メソッドで関数の参照を加えます(第31行目)。すると、戻り値の関数が呼出されるとき、その引数に関数自身の参照が加えられることになるのです(第32行目)。

  1. function proxy(method, scope) {
  2.   var aArgs = Array.prototype.slice.call(arguments, 2);
  3.   var myFunction = function () {
  4.     aArgs.push(myFunction);
  5.     return method.apply(scope, Array.prototype.slice.call(arguments, 0).concat(aArgs));
  6.   };
  7.   return myFunction;
  8. }

この新たなproxy()関数を使えば、前掲コード002は以下のようにすっきりします。イベントリスナー(listener)を定める前に空のオブジェクトをつくったり、関数の参照をそのプロパティに与えたりしなくて済みます。ただちに、EventDispatcher.addEventListener()メソッドの第2引数に渡して構いません(第11〜12行目)。

リスナー関数(release())は、引数に関数自身の参照を受取ります。ただし、proxy()関数(SoundJSのproxy()メソッドも同じ)は第3引数以降を、Function.bind()メソッドとは逆に、もとの関数が受取る引数の後に加えます(第14行目)。後は、引数(listener)に受取ったリスナー関数を、EventDispatcher.removeEventListener()メソッドで除けばよいだけです(第17行目)。

  1. function press(eventObject) {
      /*
      var item = {};
      var listener = release.bind(eventObject, item);
      item.listener = listener;
      */
  2.   var listener = proxy(release, eventObject);
  3.   eventObject.addEventListener("mouseup", listener);
  4. }
    // function release(item, eventObject) {
  5. function release(eventObject, listener) {
      // var listener = item.listener;
  1.   this.removeEventListener("mouseup", listener);
  1. }

JavaScriptコードの全体は、以下のコード003のとおりです。コード001や002と比べて、簡潔な書き方になりました。ただし、Function.bind()メソッドと同じく、新たな関数をつくってリスナーに定めますので、そのメモリは余分に費やします。もとの関数をそのまま用いるコード001の方が、メモリから見ると経済的(エコ)です。スコープを任意に定められる利点と考え合わせて使い分ければよいでしょう。

コード003■リスナー関数の引数にリスナー自身の参照を加えてイベントから削除した
  1. var stage;
  2. function initialize() {
  3.   var canvasElement = document.getElementById("myCanvas");
  4.   var instance = createRectangle(0, 0, 20, 20);
  5.   stage = new createjs.Stage(canvasElement);
  6.   stage.addChild(instance);
  7.   instance.addEventListener("mousedown", press);
  8.   stage.update();
  9. }
  10. function press(eventObject) {
  11.   var listener = proxy(release, eventObject);
  12.   eventObject.addEventListener("mouseup", listener);
  13. }
  14. function release(eventObject, listener) {
  15.   var result = [];
  16.   result.push(this.hasEventListener("mouseup"));
  17.   this.removeEventListener("mouseup", listener);
  18.   result.push(this.hasEventListener("mouseup"));
  19.   alert(result);   // 表示: true, false
  20. }
  21. function createRectangle(x, y, width, height) {
  22.   var myShape = new createjs.Shape();
  23.   var myGraphics = myShape.graphics;
  24.   myGraphics.beginFill("blue");
  25.   myGraphics.drawRect(x, y, width, height);
  26.   return myShape;
  27. }
  28. function proxy(method, scope) {
  29.   var aArgs = Array.prototype.slice.call(arguments, 2);
  30.   var myFunction = function () {
  31.     aArgs.push(myFunction);
  32.     return method.apply(scope, Array.prototype.slice.call(arguments, 0).concat(aArgs));
  33.   };
  34.   return myFunction;
  35. }


作成者: 野中文雄
更新日: 2013年4月25日 若干の字句の修正。
作成日: 2013年3月27日


Copyright © 2001-2013 Fumio Nonaka.  All rights reserved.