サイトトップ

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

Adobe Flash非公式テクニカルノート

リスナー関数が使う値を外から指定する

ID: FN1007002 Product: Flash CS3 and above Platform: All Version: 9 and above/ActionScript 3.0

イベントリスナーに登録する関数で、たとえばその登録のときに指定した値を使いたいことがあります。その手法をいくつかご紹介して説明します。


01 リスナー関数に引数を渡すのではない
リスナー関数にどうしたら引数が渡せるのか、と問われることもときおりあります。しかし、それは大抵の場合、問題の立て方を誤っています。ActionScript 3.0の定義済みの仕組みでは、リスナー関数を呼出すのはFlash Playerであり、引数はイベントオブジェクトひとつです。

リスナー関数に引数を渡そうとするのは、店から直送されるお中元に、手書きのメッセージを添えたいというのと同じです。商品を送るのはお店で、メッセージは自分の手元です。そのふたつに接点がないのですから、同封しようがありません。どうしてもやりたければ、品物を自分のもとに送ってもらって、メッセージを入れたうえで、改めて発送するしかないでしょう。

ActionScript 3.0では、中継のクラスを定義したうえでイベントリスナーに登録し、そのクラスから改めて関数に引数を渡すか、カスタムイベントを配信するという設計になります。けれど、大抵はイベント配信の流れまで変えたいというのではなく、予め指定した値をリスナー関数で利用できれば済むようです。


02 入れ子の関数とローカル変数を利用する − 関数クロージャ
前項01のリスナー関数に引数を渡したいという問いに対する答えとして、よく使われるのがいわゆる「関数クロージャ」を使う手法です[*1]。これは、設定用に定められた関数の中で、渡したい値をローカル変数に納め、リスナーとして用いる入れ子の関数を定義して返します。

つぎのフレームアクションは、Stageオブジェクトへのマウスクリックに対するイベントリスナーを、関数(getListener())から受取って登録します。関数(getListener())には渡す値としてDateインスタンスを引数に指定し、入れ子で定義した名前のない関数が戻り値になっています。ステージをクリックすると、イベントリスナーを登録するときにつくったDateインスタンスの文字列表現が[出力]されます。

stage.addEventListener(MouseEvent.CLICK, getListener(new Date()));
function getListener(param:Object):Function {
  return function (eventObject:Event):void {
    trace(this, param);   // 出力: [object global] Tue Jul 20 00:00:00 GMT+0900 2010
  }
}

スクリプトの行数は少なくて済み、関数に引数を渡している「気分」がウケているようです。しかし、上述のとおり、値はイベントが発生(クリック)したときではなく、リスナーを登録したときに設定されたものです。しかも、これから述べるように、クセや注意点が少なくないので、とくに必要がある場合でないかぎり、お使いになることはあまりお勧めしません。

第1に注意しておきたいのは、イベントリスナーに登録した入れ子関数(クロージャ)におけるthis参照です。thisキーワードは、関数やメソッドが定義されたオブジェクト(クラスのインスタンス)を参照します。上記フレームアクションでは、trace()関数の第1引数にthisを指定しました。その[出力]は[object global]です。

[object global]は、グローバルオブジェクトを示します[*2]。これは、入れ子関数(クロージャ)が、特定のオブジェクトに定義されていないことを意味します。いわば、根なし草の関数なのです。

第2は、クロージャの意味と働きです。上記フレームアクションでは、イベントリスナーに入れ子関数を登録しました。そして、入れ子関数は親関数(getListener())の引数(param)を[出力]しています。それができるのは、入れ子関数の参照には、親関数の引数やローカル変数が含まれているからです。このように関数とその定義された環境をひとつのデータとして扱うのが「クロージャ」という仕組みなのです(注[*1]参照)。

使いでがあるのは、同じシンボルのインスタンスを複数タイムラインに置いて、それぞれに異なった値を設定しつつ、リスナー関数の処理は同じにしたいというときでしょう。たとえば、3つのインスタンス(instance0〜instance2)にそれぞれ異なった整数(0〜1)を設定して、インスタンスがクリックされたらその値を[出力]してみます。その場合、つぎのようなforループの処理で、すっきりとまとめられます。

var instances:Array = [instance0, instance1, instance2];
var nLength:uint = instances.length;
for (var i:uint = 0; i < nLength; i++) {
  var instance:InteractiveObject = instances[i];
  instance.addEventListener(MouseEvent.CLICK, getListener(i));
}
function getListener(param:Object):Function {
  return function (eventObject:Event):void {
    trace(eventObject.currentTarget.name, param);
  }
}

しかし第3の注意は、見た目はすっきりでも、中身はもっさりだということです。上記forループの処理を実際に書出すと、つぎのようなスクリプトとほぼ同じになります。

instance0.addEventListener(MouseEvent.CLICK,
  function (eventObject:Event):void {
    var param:uint = 0;
    trace(eventObject.currentTarget.name, param);
  }
);
instance1.addEventListener(MouseEvent.CLICK,
  function (eventObject:Event):void {
    var param:uint = 1;
    trace(eventObject.currentTarget.name, param);
  }
);
instance2.addEventListener(MouseEvent.CLICK,
  function (eventObject:Event):void {
    var param:uint = 2;
    trace(eventObject.currentTarget.name, param);
  }
);

リスナー関数が、インスタンスと同じ数だけつくられています。目的が「それぞれに異なった値を設定しつつ、リスナー関数の処理は同じにしたい」のだとすれば、リスナー関数はひとつで済ませるのが筋でしょう。

第4にもっとも大きな問題が、関数の中で定義されたクロージャは、関数の外から参照できないことです[*3]。具体的にはこのままでは、イベントリスナーが削除できません。そのためには、リスナー関数の参照が必要になるからです。

ただでさえメモリを無駄にくううえに、消せなくなるのは避けなければなりません。リスナー関数の参照は、別にとっておくべきです。つぎのフレームアクションは、リスナー関数を配列(listeners)に納めています。そして、イベントが起こると、配列からそのリスナー関数の参照を取出して削除します[*4]

var instances:Array = [instance0, instance1, instance2];
var listeners:Array = [];   // リスナー関数を納める
var nLength:uint = instances.length;
for (var i:uint = 0; i < nLength; i++) {
  var instance:InteractiveObject = instances[i];
  var listener:Function = getListener(i)
  instance.addEventListener(MouseEvent.CLICK, listener);
  listeners.push(listener);
}
function getListener(param:Object):Function {
  return function (eventObject:Event):void {
    var target:InteractiveObject = eventObject.currentTarget;
    var listener:Function = listeners[param];
    trace(target.name, param);
    target.removeEventListener(MouseEvent.CLICK, listener);   // イベントリスナーを削除
    delete listeners[param];
  }
}

第5につけ加えるなら、クロージャに含まれるローカル変数(引数)も、外からアクセスできません[*5]。したがって、その値は後から変えられないということです。

[*1]「関数クロージャ」という用語は、あまり広くは使われないようです。一般には、単に「クロージャ」(closure)と呼ばれ、関数とそれが定義された環境(関数の定義されたオブジェクトや関数外のActivationオブジェクトなど)を一体として扱うデータ構造だとされます。たとえば、はてなダイアリー「クロージャ」、Wikipedia「クロージャ」、ITmediaエンタープライズ「クロージャとオブジェクトの微妙な関係」、Martin Fowler's Bliki in Japanese「Closure」などをご参照ください。

[*2] [ヘルプ]の[ActionScript 3.0の学習] > [ActionScript言語とシンタックス]「関数のスコープ」の「スコープチェーン」には、つぎのように説明されています。

グローバルオブジェクトは、ActionScriptプログラムが起動すると作成され、すべてのグローバル変数および関数を含みます。

[*3] 実は、もうひとつ大きな問題を引き起こしかねないことがあります。本文にも述べたとおり、クロージャには親の関数のローカル変数(引数)が含まれます。それは、入れ子関数で使うか使わないかを問いません。

つまり、要らないビットマップのデータやエレメントの多い配列、あるいは大量の文字列などがローカル変数に納められていれば、クロージャの中に参照が残ってしまうのです。しかも、ループ処理などしていたら、その回数分データが複製されます。この問題については、www.imajuk.swf「アクティベーションオブジェクトによるメモリリーク」が詳しいです。

[*4] リスナー関数自身からイベントリスナーを削除するのであれば、関数の参照はarguments.calleeプロパティで得られます。

function getListener(param:Object):Function {
  return function (eventObject:Event):void {
    var target:InteractiveObject = eventObject.currentTarget
    // var listener:Function = listeners[param]
    trace(target.name, param);
    // target.removeEventListener(MouseEvent.CLICK, listener);
    target.removeEventListener(MouseEvent.CLICK, arguments.callee);
  }
}

[*5] 関数が呼出されると、その実行の間引数やローカル変数を納めるActivationオブジェクトがつくられます。前出注[*2]の「スコープチェーン」は、つぎのように説明します。

最初に、アクティベーションオブジェクトという特別なオブジェクトが作成され、そこには関数本体で宣言されるパラメーターとローカル変数または関数が格納されます。アクティベーションオブジェクトは内部メカニズムであるため、そこに直接アクセスすることはできません。

なお、この説明でActivationオブジェクトに格納される「関数」というのが、入れ子の関数を指すと考えられます。


03 リスナー関数はひとつで済ます
インスタンス「それぞれに異なった値を設定しつつ、リスナー関数の処理は同じにしたい」というお題で、リスナー関数はひとつで済ませましょう。前述のスクリプトと同じく、インスタンスはタイムラインに置いた3つ(instance0〜instance2)で、それぞれに異なる整数(0〜1)を割当てるものとします。

まず、インスタンスがMovieClipなら簡単です。MovieClipインスタンスには、いくらでも変数が設定できるからです。たとえば、つぎのフレームアクションのように処理すればよいでしょう。インスタンスをクリックしたらリスナー関数を呼出し、イベントリスナーからは削除します。

var instances:Array = [instance0, instance1, instance2];
var nLength:uint = instances.length;
for (var i:uint = 0; i < nLength; i++) {
  var instance:MovieClip = instances[i];
  instance.param = i;   // インスタンスに変数を設定
  instance.addEventListener(MouseEvent.CLICK, listener);
}
function listener(eventObject:Event):void {
  var instance:MovieClip = MovieClip(eventObject.currentTarget);
  trace(instance.name, instance.param);
  instance.removeEventListener(MouseEvent.CLICK, listener);
}

つぎに、ボタンシンボル(SimpleButtonクラス)やSpriteクラスなど、多くのクラスでは変数(プロパティ)が勝手に加えられません。すると、変数はフレームアクションで別に定めることになりそうです。できれば、配列などにまとめられると扱いやすいでしょう。

しかし、配列からインスタンスごとに異なったエレメントを取出すには、それぞれに異なったインデックスをもたせなければなりません。けれど、インスタンスごとに異なった値が設定できないから困っているので、これでは堂々巡り、童謡「バケツの穴」状態です。

幸い、すでにインスタンスごとに違うものはあります。まずは、インスタンス名(instance0〜instance2)です。文字列の名前(DisplayObject.nameプロパティ)をキーにして値をまとめるには、Objectインスタンスが使えます。以下のフレームアクションは、3つのインスタンス名の文字列に対してそれぞれ設定した値を、Objectインスタンスに加えています。リスナー関数は、イベントを処理するインスタンス名からObjectインスタンスに納められた値を取出して[出力]します。

var instances:Array = [instance0, instance1, instance2];
var params:Object = {};   // Objectインスタンスを生成
var nLength:uint = instances.length;
for (var i:uint = 0; i < nLength; i++) {
  var instance:MovieClip = instances[i];
  params[instance.name] = i; // インスタンス名をキーとしてObjectインスタンスに値を納める
  instance.addEventListener(MouseEvent.CLICK, listener);
}
function listener(eventObject:Event):void {
  var instance:MovieClip = MovieClip(eventObject.currentTarget);
  trace(instance.name, params[instance.name]);
  instance.removeEventListener(MouseEvent.CLICK, listener);
}

もっとも、ActionScript 3.0では文字列のインスタンス名は、扱いやすいとはいえず、また応用の幅が広くありません[*6]。インスタンスの名前でなく、参照そのものを用いることが、できれば望ましいでしょう。参照をキーとして値をまとめられるのは、Dictionaryクラスです。

Dictionaryクラスの使い方について、詳しくは「Dictionary()コンストラクタ」をお読みください。具体的には、上述のスクリプトで、Objectインスタンスの生成をDictionaryに替え、インスタンスからの値の取得は名前でなく参照をキーにします。

var instances:Array = [instance0, instance1, instance2];
var params:Dictionary = new Dictionary();   // Dictionaryインスタンスを生成
var nLength:uint = instances.length;
for (var i:uint = 0; i < nLength; i++) {
  var instance:MovieClip = instances[i];
  params[instance] = i;   // 参照をキーとしてDictionaryインスタンスに値を納める
  instance.addEventListener(MouseEvent.CLICK, listener);
}
function listener(eventObject:Event):void {
  var instance:MovieClip = MovieClip(eventObject.currentTarget);
  trace(instance.name, params[instance]);
  instance.removeEventListener(MouseEvent.CLICK, listener);
}

これだけでは、ObjectとDictionaryクラスを使う違いが、さほどないように思えるかもしれません。しかし、インスタンスをタイムラインに動的に置くときは、とくに必要がないかぎり名前はつけないでしょう。また、Objectクラスを使った場合、インスタンスからいちいち名前を取出さないと値が得られません。

[*6] とくに以前のバージョンのActionScriptとの違いも含めて、F-site「MovieClipインスタンスとインスタンス名」をご参照ください。


04 中継のクラスから関数に引数を渡す
前項03の手法で、おそらく課題は解決できると思います。しかし、どうしても引数を渡したいという場合について、試みに考えてみます。01で述べた「中継のクラスを定義したうえでイベントリスナーに登録し、そのクラスから改めて関数に引数を渡す」手法です。つぎのような中継のクラスPassingParamToListenerを定義しました。

package {
  import flash.events.Event;
  public class PassingParamToListener {
    public var param:Object;
    public var listener:Function;
    public function PassingParamToListener(myParam:Object, myListener:Function) {
      param = myParam;
      listener = myListener;
    }
    public function callListener(eventObject:Event) {
      if (listener is Function) {
        listener(eventObject, param, this);
      }
    }
  }
}

このクラスの使い方は、つぎのようになります。第1に、コンストラクタメソッドに、渡したい値(myParam)と呼出す関数(myListener)を引数として渡します。第2に、イベントリスナーにはこのPassingParamToListenerクラスのインスタンスメソッドcallListener()を登録します。すると、リスナーメソッドcallListener()が中継して、指定された関数に引数を渡して呼出すという仕組みです。

var caller:PassingParamToListener = new PassingParamToListener(myParam, myListener);
instance.addEventListener(MouseEvent.CLICK, caller.callListener);

タイムラインに置かれた3つのインスタンス(instance0〜instance2)のお題に当てはめるなら、以下のフレームアクションのようになります。

var instances:Array = [instance0, instance1, instance2];
var nLength:uint = instances.length;
for (var i:uint = 0; i < nLength; i++) {
  var instance:MovieClip = instances[i];
  var caller:PassingParamToListener = new PassingParamToListener(i, listener);
  instance.addEventListener(MouseEvent.CLICK, caller.callListener);
}
function listener(eventObject:Event, param, caller):void {
  var instance:MovieClip = MovieClip(eventObject.currentTarget);
  trace(eventObject.currentTarget.name, param);
  instance.removeEventListener(MouseEvent.CLICK, caller.callListener);
}

PassingParamToListenerクラスのコンストラクタに渡した関数は、呼出されるとき3つの引数を受取ります。第1引数はイベントオブジェクト、第2引数がコンストラクタに指定した値、そして第3引数はPassingParamToListenerインスタンスです。この第3引数を用いれば、上記のとおりイベントリスナーが削除できます[*7]

[*7] PassingParamToListenerインスタンスはあえて参照を残していませんので、イベントリスナーから削除されればガベージコレクトを待つことになります。


作成者: 野中文雄
作成日: 2010年7月21日


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