サイトトップ

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

HTML5テクニカルノート

CreateJSで関数にスコープを定める ー proxy()関数

ID: FN1303001 Technique: HTML5 and JavaScript Library: SoundJS 0.5.0 / PreloadJS 0.4.0

proxy()関数
文法 proxy(method, scope, arg)
概要 [静的] スコープが定められた新たな関数を返す。
引数

method − スコープ(this参照)を定める関数。

scope − 関数内のスコープ(this参照)として定めるオブジェクト。

arg − 呼出した関数に渡したい引数があれば、任意の数加えられる(オプション)。

戻り値 定められたオブジェクトを関数内のスコープ(this参照)とした新たな関数。

説明

proxy()はCreateJSにグローバルに定められた関数で、オブジェクトもクラスも参照せず、名前空間(createjs)に続けて直接呼出します。SoundJSとPreloadJSのふたつのライブラリに、JavaScript(JS)ファイルProxy.jsとして加えられています。それぞれのコンパクト(min)版JSファイルにも含まれます。

JavaScriptでは、関数もプロパティに含まれます。そして、オブジェクトのプロパティに加えた関数は、そのオブジェクトのメソッドとみなされます。つまり、オブジェクトに与えた関数を呼出すと、関数本体のthisキーワードはそのオブジェクトを参照します。関数が定められたスコープは保たれないのです。

CreateJSのproxy()関数は、第1引数の関数に第2引数のスコープを定めた新たな関数を返します。戻り値の関数は、どのオブジェクトのもとで呼出されても、予め定めたスコープが関数本体のthis参照となります。

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

たとえば、つぎのJavaScriptコードは、関数(method())をオブジェクト(instance)に定めました。すると、同じ関数を直に呼んだときと、オブジェクトのメソッドとして呼出したときとでは、thisの参照が異なることになります(図001)。

var variable = "global";
var instance = {};
instance.variable = "instance";
instance.method = method;
alert([method(), instance.method()]);   // global,instance
function method() {
  return this.variable;
}

図001■関数を直に呼んだときとオブジェクトのメソッドとして呼出したときとではthis参照が変わる
図001

proxy()関数を使うと、第1引数の関数をつねに第2引数のスコープで呼出すことができます。戻り値の関数はどのオブジェクトのメソッドに与えても、proxy()関数の第2引数で決めたオブジェクトがthis参照されて呼出されます。

つぎのコードはscript要素にSoundJS 0.5.0を読込みました[*1]。そして、オブジェクト(instance)のメソッド(method)に、proxy()関数でスクリプトの記述場所(this)をスコープに定めた関数が与えられました。そのため、オブジェクトのメソッドとして呼んでも、直に呼出しても、this参照は変わりません(図002)[*2]

<script src="http://code.createjs.com/soundjs-0.5.0.min.js"></script>
<script>
var variable = "global";
var instance = {};
instance.variable = "instance";
instance.method = createjs.proxy(method, this);
alert([method(), instance.method()]);   // global,global
function method() {
  return this.variable;
}
</script>

図002■関数を直に呼んでもオブジェクトのメソッドとして呼出してもthis参照は変わらない
図002

proxy()関数に渡した第3引数以降は、戻り値の関数を呼出したときその引数に加えられます。たとえば、つぎのようにオブジェクト(myObject)のイベント("event")にリスナーとしてproxy()関数の戻り値を加えると、イベントが起こったときに第3引数以降(arg1とarg2)をリスナー関数は引数として受取ります。ただし、リスナー関数が呼出されるとき引数として渡されるイベントオブジェクト(eventObject)の後に加わります[*3]

myObject.addEventListener("event", createjs.proxy(myHandler, this, arg1, arg2));

function myHandler(eventObject, arg1, arg2) {
  // イベント"event"でリスナー関数として呼出される
}

proxy()関数の戻り値となる関数は、第1引数の関数にもとづいて新たにつくり直されます。つまり、第1引数の参照とは異なる関数です。そのため、proxy()関数で定めたリスナーを削除するには、その参照を得なければなりません。EventDispatcher.addEventListener()メソッドは加えたリスナーの参照を返すので、それをもっておけばよいでしょう。

var listener = myObject.addEventListener("event", createjs.proxy(myHandler, this, arg1, arg2));

function myHandler(eventObject, arg1, arg2) {
  // イベント"event"でリスナー関数として呼出される
  myObject.removeEventListener("event", listener);
}

[*1] SoundJS 0.4.0にもproxy()関数は備わっています。ただし、0.5.0とは異なってSoundクラスの中で定められていたり、「CreateJSのproxy()メソッドに第3引数が渡せない」という問題がありました。

[*2] 本文の例では、関数内でthisキーワードを使わなければ、いずれもグローバルに定めた変数が参照されます。

var variable = "global";
var instance = {};
instance.variable = "instance";
instance.method = method;
alert([method(), instance.method()]);
function method() {
  // return this.variable;
  return variable;   // global,global
}

ただし、同じ名前のローカル変数があると、その値が参照されてしまいます。この場合、グローバルな変数の値を得るには、参照先を定めなければなりません。

function method() {
  var variable = "local";
  return variable;   // local,local
}

[*3] 「SoundJS v0.5.0 API Documentation」のproxy()メソッドの項で「Example」に掲げられた例は、リスナー関数(myHandler())の引数にイベントオブジェクトが定められていません(図003)。この場合、第1引数はイベントオブジェクトを受取り、proxy()メソッドで与えたふたつの引数の2番目が受取れません。

図003■「SoundJS v0.5.0 API Documentation」のproxy()メソッドの「Example」に掲げられた例
図003


HTMLドキュメントに加えたボタンで、サウンドの停止と再生をしてみましょう(図004)。以下のコードのように、ボタンはbody要素にinput要素で加え、type属性を"button"に定めます。また、JavaScriptで参照を得るため、id属性に識別子("control")を与えておきます。

図004■HTMLドキュメントにボタンを加えた
図004

<body>
  <input type="button" value="loading..." id="control" />
</body>

script要素としては、以下のコード001を書き加えます。ひとつのボタンで、停止と再生を切替えます。それぞれの設定は、ふたつの関数(readyToStop()とreadyToPlay())で定めました。どちらも、サウンドを操作してボタンクリックのハンドラを切替えるため、引数にSoundInstanceオブジェクトとボタンのふたつの参照が求められます(第16および第23行目)。

サウンドが再生し終わったときのSoundInstance.completeイベントには、proxy()関数で第1引数の関数(soundCompleted())のスコープを第2引数のボタン(control)にしてリスナーに定めました(第8〜9行目)。

リスナー関数(soundCompleted())からは、ボタンの設定を再生にする関数(readyToPlay())を呼出します(第14行目)。このとき、SoundInstanceオブジェクトの参照は、引数に受取ったイベントオブジェクト(eventObject)のtargetプロパティから得られます(第13行目)。そして、proxy()関数によりthis参照がボタンになっていますので、関数に渡すふたつの引数が揃います。

このようにして、SoundInstanceオブジェクトとボタンのどちらの参照もグローバルな変数に定めることなく、サウンドの停止と再生のコントロールが行われるのです。

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

以下のコード002は、オブジェクトにふたつのメソッドを加えます。ただし、proxy()関数により、そのスコープは別のオブジェクトに定めます。そのうえで、ふたつのメソッドを呼出して、それぞれのthis参照がどう変わったか確かめます。

ふたつのオブジェクト(pointとtemplate)には、xy座標を想定したプロパティ(xとy)が加えられています(第4〜5行目)。そのうちのひとつのオブジェクト(template)に、メソッドをふたつ(transfer()とtoString())与えます。ただし、proxy()関数で、それらのスコープはもうひとつのオブジェクト(point)に定めました(第6〜7行目)。

オブジェクト(template)に与えたひとつ目のメソッド(transfer())は、xy座標を原点(0, 0)に対して伸縮・回転します。第1引数(scale)が伸縮比率、第2引数を回転角(rotation)として、関数(transfer())で座標変換します(第10〜20行目)。その関数をオブジェクトのメソッドとして、ふたつの引数(2, 60)で呼出します(第8行目)。

オブジェクト(template)のふたつ目のメソッド(toString())には、EaselJSのPoint.toString()メソッドを譲受けました(第7行目)。オブジェクトは文字列表現が求められると、内部的にそのtoString()メソッドを呼出して戻り値の文字列が用いられます。配列がWindow.alert()メソッドで警告ダイアログに表示されるとき、それぞれの配列エレメントは文字列に変換されます(第9行目)。なお、Point.toString()メソッドは、つぎのような文字列を返します。

[Point (x=xプロパティ値 y=yプロパティ値)]

オブジェクト(template)のメソッドふたつ(transfer()とtoString())は別のオブジェクト(point)をスコープに定めたので、メソッドで操作されるのはスコープのオブジェクトで、呼出したオブジェクトは何も変わりません。

コード002■オブジェクトのメソッドを別のオブジェクトのスコープで呼出す
  1. <script src="http://code.createjs.com/easeljs-0.7.0.min.js"></script>
  2. <script src="http://code.createjs.com/soundjs-0.5.0.min.js"></script>
  3. <script>
  4. var point = {x:1, y:0};
  5. var template = {x:0, y:0};
  6. template.transfer = createjs.proxy(transfer, point);
  7. template.toString = createjs.proxy(createjs.Point.prototype.toString, point);
  8. template.transfer(2, 60);
  9. alert([template.x, template.y, template]);
  10. function transfer(scale, rotation) {
  11.   var x = this.x;
  12.   var y = this.y;
  13.   var radians = rotation * createjs.Matrix2D.DEG_TO_RAD;
  14.   var sin = Math.sin(radians);
  15.   var cos = Math.cos(radians);
  16.   x *= scale;
  17.   y *= scale;
  18.   this.x = cos * x - sin * y;
  19.   this.y = sin * x + cos * y;
  20. }
  21. </script>

警告ダイアログボックスには、結果を納めた配列がつぎのように示されます(図005)。初めのふたつのエレメントは、メソッドを与えたオブジェクト(template)のxy座標のプロパティ値です。もとの値(0, 0)から変わっていません。

0,0,[Point (x=1.0000000000000002 y=1.7320508075688772)]

配列の3つ目のエレメントには、オブジェクト(template)そのものが納められています。すると、前述のとおり、エレメントであるオブジェクトの文字列表現(toString()メソッドの戻り値)が示されます。ところが、文字列に換えるスコープが別のオブジェクト(point)なので、そのオブジェクトの座標(1, 0)がひとつ目のメソッド(transfer())で変換(伸縮2、回転60度)された結果の値(1, √3)になっています(実際には、わずかな誤差が含まれます)。

図005■警告ダイアログボックスにふたつのオブジェクトの情報が示された
図005

ご参考までに、座標変換の関数(transfer())について、簡単に解説を加えます(この処理内容はproxy()関数には関わりません)。関数がオブジェクトのメソッドとして呼出される前提とします。

前述のとおり、座標変換の関数(transfer())は、第1引数(scale)が伸縮率、第2引数(rotation)は回転角です(第10行目)。まず、オブジェクトの座標プロパティ値を変数(xとy)にとります(第11〜12行目)。メソッドが受取る第1引数の伸縮率は、ふたつのプロパティ値に乗じます(第16〜17行目)。

つぎに、メソッドの第2引数(rotation)にもとづく角度(θ)の回転は、以下の計算式でxy座標を変換します(詳しくは「三角関数で座標を回転するふたつの計算方法」をお読みください)。なお、三角関数は角度にラジアンを用いるため、定数Matrix2D.DEG_TO_RADで予め度数から変換しています(第13行目)。

x座標: x cosθ - y sinθ
y座標: x sinθ + y cosθ

三角関数の値は予め変数(sinとcos)に与えます(第14〜15行目)。そして、上記の式で求めた値を、オブジェクトに新たなプロパティ値として定めています(第18〜19行目)。これで、オブジェクトの座標が、原点から伸縮および回転されます。


実装

CreateJSの関数proxy()は、Soundクラスの中でつぎのコード003のように実装されています。用いられているおもなメソッドとともに、以下にかいつまんでご説明します(抜書きする行番号はコード003にもとづきます)。

コード003■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()関数は関数を返す

proxy()関数はつぎのように、returnステートメントで名前のない関数(function)をつくって返します(第3〜5行目)。この戻り値の関数が、proxy()関数の第1引数の関数(method)を第2引数のスコープ(scope)で呼出す新しい関数です。したがって、その新たな関数は、メソッド(method)をスコープ(scope)で呼出し、その結果を返すということになります(第4行目)。

  1. createjs.proxy = function (method, scope) {
  1.   return function () {
  2.     return /* methodをscopeで呼出した結果 */;
  3.   };
  4. }
●Functionクラスのスコープを変えるメソッド

関数のスコープを変えて呼出すメソッドが、Functionクラスに備わっています。それが、Function.apply()メソッドです。関数(Functionオブジェクト)を参照して呼出します。第1引数はスコープ、第2引数には関数に渡す引数を配列で定めます。

関数.apply(スコープ, 関数に渡す引数の配列)

proxy()関数の戻り値である新たな関数は、Function.apply()メソッドにより、proxy()関数の第1引数の関数(method)を第2引数のスコープ(scope)で呼出して返しています(第4行目)。

  1. return method.apply(scope, /* methodに渡す引数の配列 */);

Function.apply()メソッドの第2引数には、参照する関数に渡す引数を配列に入れて渡します。proxy()関数の他の処理は、おもにこの引数に渡す配列をつくっています。

なお、Functionクラスには、関数のスコープを変えて呼出すもうひとつのメソッドFunction.call()があります。Function.apply()メソッドとの違いは、参照もとの関数に渡す引数をひとつの配列ではなく、その数だけ第2引数以降に定めることです。

関数.call(スコープ, 関数に渡す引数, …)

proxy()関数の第1引数に受取る関数は、いくつの引数が渡されるのか予め決まっていません。このような場合は、Function.apply()メソッドを用いると、引数がいくつあってもひとつの配列に納めて渡せます。

●argumentsオブジェクトをArrayクラスのメソッドで扱う

argumentsは関数が自動的につくるローカル変数です。配列に似たオブジェクトが与えられ、関数の受取った引数を連番インデックスのエレメントとしてもちます。

ただし、argumentsオブジェクトには配列のメソッドが備わっていません。配列のメソッドが使いたいときには、Function.call()メソッドにより、Arrayクラスのメソッドをargumentsオブジェクトのスコープで呼出します。一般に、クラスに備わるメソッドを参照するときには、クラスのFunction.prototypeプロパティからメソッドを得ます。

クラス.prototype.メソッド.call(スコープ, 引数, …)

proxy()関数では、まずargumentsオブジェクトに対してArray.slice()メソッドを呼出しています(第2行目)。このメソッドは、配列インデックスの始まりと終わりの整数をふたつ引数に渡すと、その範囲のエレメントが納められた新たな配列を返します。

argumentsオブジェクトの第2インデックスは、関数が受取る第3引数です。けれど、proxy()関数には第2引数までしか定められていません。argumentsオブジェクトは、関数に定めがなくても、呼出されたとき受取った引数すべてをエレメントに納めます。

Array.slice()メソッドの第2引数に終わりのインデックスを渡さないと、始まりに定めた第1引数のインデックスから最後までのエレメントが取出されます。つまり、argumentsオブジェクトに第3引数以降が渡されれば、そのすべてを新たな配列として変数(aArgs)に納めます。

  1. createjs.proxy = function (method, scope) {
  2.   var aArgs = Array.prototype.slice.call(arguments, 2);
  1. }

proxy()関数が返す新たな関数は、その戻り値でFunction.apply()メソッドを呼出していました(第4行目)。そのFunction.apply()メソッドに渡す第2引数の配列が、つぎのようにargumentsオブジェクトをもとにつくられています。このargumentsオブジェクトは、proxy()関数ではなく、メソッドが返す新たな関数が受取ることにご注意ください。

argumentsオブジェクトのスコープでArray.slice()メソッドを呼出す処理は、すでにご説明しました。けれど、ここでは始まりインデックスに0を渡しています。つまり、argumentsオブジェクトのエレメントすべてをそっくりそのままということで、意味がないように感じられるかもしれません。しかし、返されるのは新たな配列なので、Arrayクラスのメソッドが使えるようになるのです[*4]。そこで、Array.concat()メソッドを呼出して、proxy()関数が受取った第3引数以降(aArgs)を加えています。

Array.prototype.slice.call(arguments, 0).concat(aArgs)

このようにしてproxy()関数から、第1引数の関数を第2引数のスコープで呼出す新たな関数が返されるのです。新たな関数には、もとの関数に定められた引数に加え、proxy()関数に与えた第3引数以降が渡されます。

[*4] Array.slice()メソッドの第1引数も省くと、始まりのインデックスは0とみなされます。したがって、Function.apply()メソッドに第2引数を渡さなくても構いません(MDN「arguments」「説明」参照)。

Array.prototype.slice.call(arguments)


作成者: 野中文雄
更新日: 2013年10月27日 タイトルを「SoundJSで関数にスコープを定める ー proxy()メソッド」から変更。CreateJSの新バージョンにもとづいて、全体にわたり加筆・補正した。
更新日: 2013年3月20日 細かな字句の修正。
作成日: 2013年3月18日


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