今回は AS3 のthis と Java の this はよく似ているという話です。
ActionScript のようなプロトタイプベースの言語では、this の扱いが少々分かりにくくなります。 たとえば以下のような例を考えて見ましょう。
public class ClassA { public var myName:String = "classA"; public function foo() { trace(this.myName); } } public class ClassB { public var myName:String = "classB"; public var bar:Function; } var a:ClassA = new ClassA(); var b:ClassB = new ClassB(); b.bar = a.foo; b.bar(); // this は a と b どっちを指す?
まず ClassA の定義を見ると foo という関数内で this が使用されています。この this はコンパイル時のスコープを基準にすれば ClassA のインスタンスを指すはずです。
一方、 サンプルの下4行では、同じ関数を ClassB のインスタンスが参照する関数として呼び出しています。従って、実行時を基準にすると this は b すなわち ClassB のインスタンスを指すようにも思われます。
呼び出し方によって this の指すものが変わってしまったらプログラムの動作を理解するのが難しくなってしまいますし、せっかくのコンパイラによる型チェックも無駄になってしまいます。しかしプロトタイプベースの言語では上のコードのような状況が可能です。
そこで、AS3 では this が常に所属するインスタンスを指すための仕組みを提供しています。
b から呼び出された場合でも関数 foo 内の this が a を指せるためには、関数 foo の定義情報とそれに加えて関数定義を評価するための環境(例えば ClassA 内の他のプロパティやメソッド等の情報)が必要になります。これをクロージャといいます。
AS3 では自クラス内で定義されているメソッドへの参照を他のインスタンスに渡す際に、自動的にクロージャが作成されます。このため、上記サンプルコード内の最終行の出力は classA になります。AS2 では classB が出力されていたところです。
メソッドと Function 型プロパティと無名関数
ところで関数本体の定義方法によって上で説明したようなクロージャが生成されないことがあります。
public class ClassC { public var myName:String = "classC"; // メソッドの形式で定義 public function mFunc() { trace(this.myName); } // Function 型プロパティとして定義 public var pFunc:Function = function() { trace(this.myName); } }
上記サンプル内では2種類の異なる方法で関数 mFunc と pFunc が定義されています。それぞれの関数を他のインスタンスから呼び出すと結果は以下のように異なります。
var b:ClassB = new ClassB(); var c:ClassC = new ClassC(); b.bar = c.mFunc; b.bar(); // classC が出力される b.bar = c.pFunc; b.bar(); // classB が出力される
pFunc の this は b を指していますね。このことから、たとえクラス定義内での記述でも Function 型のプロパティにアサインされている無名関数内の this に対するクロージャは作成されないことが分かります。
さらに、pFunc に対してはコンパイル時の this に対する評価も行われません。なので、インスタンスのメソッドとして関数を定義する際は Function 型のプロパティとして定義するのを第一の選択にするのは考えたほうがよさそうです。
Function 型のプロパティを使用する必要があるのは、実行時に関数オブジェクトを代入する場合です。メソッドの形式で定義した場合、このような目的に使うことはできません。
var c:ClassC = new ClassC(); var b:ClassB = new ClassB(); c.mFunc = b.bar; // コンパイルエラー c.pFunc = b.bar; // こちらは OK
無名関数とクロージャ
さて、AS3 ではメソッドと無名関数の扱いが明確に区別されているらしいことが分かりました。メソッドは静的なもの、無名関数は動的なものといったところでしょうか。無名関数は、特定のインスタンス(すなわち固定された this) とは関係ない使い方をするものと割り切ってよさそうです(AS2 ではこっちが基本でしたが)。実際、this を使用しなければクロージャが使用できます。
public class ClassD { public var myName:String = "classD"; public var pFunc:Function = function() { trace(myName); } } var d:ClassD = new ClassD(); var b:ClassB = new ClassB(); b.bar = d.pFunc; b.bar(); // classD が出力される
上記のように ClassD 内の pFunc にアサインされている無名関数から this を使わずに直接 myName を参照すると pFunc の定義が ClassD 内の値とカプセル化されます。この機能は、特定の状態のスナップショットが欲しい場合にはとても便利です。
メソッドと Function 型プロパティと無名関数の欄の
var c:ClassC = new ClassC();
var b:ClassB = new ClassB();
c.mFunc = b.bar; // コンパイルエラー
c.pFunc = c.bar; // こちらは OK
一番下の行は c.pFunc = b.bar; の間違いでしょうか?
そうです。ご指摘ありがとうございました。
修正しておきます。
クロージャに対してFunction.apply()(またはFunction.call())メソッドを使って、'this'参照を変更することはできないのでしょうか?
// ActionScript 3.0クラス定義ファイル: Test.as
package {
public class Test {
var name:String = "Test class";
var propertyFunction:Function = function () {
return this.name;
}
function methodFunction () {
return this.name;
}
}
}
// fla(swf): テスト用
// フレームアクション
var o0:Test= new Test();
var o1:Object = new Object();
o1.name = "Object instance";
// [1]ActionScript 3.0ではクロージャが適用
o1.propertyFunction = o0.propertyFunction;
o1.methodFunction = o0.methodFunction;
trace(o1.propertyFunction()); // Output: Object instance
trace(o1.methodFunction()); // Output: Test class
// [2]Funcion.apply()メソッドでも変わらず
trace(o0.propertyFunction.apply(o1)); // Output: Object instance
trace(o0.methodFunction.apply(o1)); // Output: Test class
野中さん、こんにちは。
this にクロージャが適用される場合(メソッドとして宣言された関数内の this)、どのオブジェクトに対して apply() してもクロージャが付いて回るため、結果は一緒ということなのだと思います。
ここは「本来実行時に参照が解決される this に対して、特定のケースのみ静的言語的な振舞いを実現するため、もともと動的言語の機能であるクロージャを使用している」という例外的な仕様のため、動的言語に慣れた人間には不思議な感じですよね。Java を普段使ってる立場の人には this が変わることのほうが気持ち悪いのでしょうが。