Flash MXにおけるスコープチェーンとメモリの浪費

- Timothee Groleau著 -

- 上野直彦、北沢 純(FACEs Project)訳 -

2003年4月8日初版(元記事)公開

導入

Flash MXは非常に強力だが、それに伴って開発者は自分のやっていることについてより意識的になる必要が出てきた。自分のスクリプトが期待通りの結果を示さなかったり、動作はするがどうしてそうなるのかわからないということがある。場合によっては自分のスクリプトの裏側ではいろいろなことが起こっているかもしれない。

そこで今日は、スコープチェーンを取り上げて、Flash MXにおいてスコープチェーンがメモリの浪費につながる仕組みを示してみよう。この記事はホントにホントの基本から始まってはいるが、基本的にはある程度ActionScriptを理解している人を想定している。ここで示されているスクリプトの多くはおそらくJavaScriptにも適用可能だが、どれもブラウザではまだ試していないので、その点については保証はできない。

 

謝辞

この記事は何か新しいことを明らかにしているわけではない。むしろ、FlashCoders' Listで私が読んだ多くのスレッド-- 主に関わっていたのは加藤達雄Casper SchuirinkPeter HallRalf BokelbergGregory Burchその他たくさんの人々 --の要約のようなものだ。

私が知る限りでは、入れ子構造の関数におけるスコープチェーンのパターンを発見したのは日本のメーリングリストの上野直彦である(訳注:正確には個人的な発見ではなく、Flash ActionScript MLでのやりとりの中で分かってきたことである。該当するスレッドの過去ログはここにある。ただしこのリンク先を含み過去ログを閲覧するにはメーリングリストに参加する必要がある。このスレッドの発言者は上野直彦、野中文雄、youich、加藤達雄、富川真也、aki(発言順・敬称略))。 彼の投稿へのリンクは持っていないが(もしもっていたら私に送ってください)、加藤達雄がその情報をFlashCoders' Listにもたらした。

この記事にある多くの実例は加藤達雄の様々な投稿からそのまま持ってきたものであり、彼がこの記事の大きなインスピレーションの元になっている。

 

ちょっとした注意

メモリ管理について

おそらく気づいているだろうが、ActionScriptでは、メモリを確保したり解放したりする必要が全くない。これは、ActionScriptという言語が、非常に高いレベルでマシンの抽象化を行いメモリを自動的に操作してくれているからだ。

メモリ管理においてよく知られている言葉に「ガーベッジコレクター」というのがある。変数やオブジェクトを生成する時に、Flashは自動的にメモリをそれらに割り当てる。ガーベッジコレクターはオブジェクトがまだプログラム中で使われているかどうかを追跡するプロセスである。もしオブジェクトがもう使われていないことを検知すると、それを削除してメモリを解放し、貴重なリソースを放出する。

ガーベッジコレクターはそれだけで一つのトピックになってしまって、この記事の範囲を越えてしまう。ガーベッジコレクターは「参照カウント」や「Mark and Sweep」といった技術でオブジェクトがまだ生きているかどうかを追跡する。オブジェクトは、メインプログラムの中でもしくはメインプログラムで現在使われている他のオブジェクトの中で、少なくとも一箇所から参照されている限り、メモリ内に残るということをこの記事では、述べていきたいと思う。

 

"「(var)はローカル変数を宣言するために使う。関数の内部でローカル変数を宣言すると、その変数はその関数のために定義されて、関数呼び出しがおわると無効になる。」"

この文章はActionScript辞書のvarの項から抜きだしてきた。 ここで言われているようにvarは関数内でローカル変数を宣言するために使われ、それらのローカル変数は関数が実行を終えた時に削除される。ここで起こるのは、ガーベッジコレクターが、メインプログラム内で参照が行われていないローカル変数を削除するということだ。これはすばらしい!

単純な実例:

コード:
1
2
3
4
5
6
test = function() {
  var a = 5;
  trace("inside: " + a);
}
test(); // inside: 5
trace("outside: " + a); // outside: 
出力:

inside: 5
outside:

このあたりは十分良く知っているところだろう。

 

ところで関数についてはこれとは別に興味深いところがある。それは、関数本体の内部から関数の外部にある変数を呼び出せるということだ。以下のようになる。:

コード:
1
2
3
4
5
a = 5;
test = function() {
  trace(a);
}
test(); // 5
出力:

5

「うん、普通だよ!」というかもしれない。でもここで本当に起こっていることはなんだろう?"a"という変数が関数本体の外にあるとき、Flashはどうやってそこにアクセスするのだろうか?答えは、Flashが関数に付加されているスコープチェーンを辿っていくということだ。

 

スコープチェーンってなに?

「スコープチェーン」という言葉は、ActionScript辞書では一箇所しか現れない。withアクションのページである。ここには非常に貴重な情報がつまっているので皆さんに読むことをお勧めしたい。ただこのページはこの記事で触れるにはいまのところまだちょっと早いので、すこし速度をゆるめよう。

それで、スコープとはなんだろう? 私にとっては、スコープはFlashが変数を探すときに調べるオブジェクトである。この単純な定義からいうと、スコープチェーンはFlashが変数を探すときに順に調べていくオブジェクトの集合である。スコープチェーンの先頭に存在するオブジェクトが、望みの変数を保持していなければ、Flashはより深いところにある次のオブジェクトへ進み、変数が見つかるかスコープチェーン内の全てのオブジェクトを調べるかするまでこのプロセスを繰り返す。

スコープチェーンを調べる目的は、明示的なスコープ無しにアクセスしようとされているプロパティを取得するためである。例えば、"trace(a);"を実行したとき、"a" がどこに存在しているかは明示的に宣言されていないので、Flashはスコープチェーンの中に"a"を探す。"trace(anyObject.a);"を実行したときは、"a"を見つけるために検索するべき明示的なスコープが存在する。スコープは"anyObject"を参照することで指定されているので、Flashは "a"を探すのに"anyObject"オブジェクトの内部をみるだけでよく、スコープチェーンの内部を見る必要がない。

Flashでは、ActionScriptのコードはタイムラインにしか書けない。タイムラインは(_rootであれその他のムービークリップであれ)ムービークリップの中にある。ActionScriptコードが書かれている場所では、スコープチェーンは少なくとも2つの要素を持っている:そのコードが書かれたカレントオブジェクト(訳注:つまりムービークリップ)と_globalオブジェクトである。以下は簡単なテスト用スクリプトである:

コード:
1
2
3
4
5
_global.a = 4;
a = 5;
trace(a); // 5
delete a;
trace(a); // 4
出力:

5
4

とりあえず、何が起こっているか一行ずつ説明してみよう:1行目では_globalオブジェクトに変数"a"を作成する。2行目ではカレントスコープに変数"a"を作成する。3行目では、"a"をtraceするために、Flashはカレントスコープで"a"を探し、見つけて、(5)と出力する。4行目は"a"をカレントスコープから削除する。5行目では、"a"をtraceするためにFlashがカレントスコープから"a"を探し、見つからないのでスコープチェーンの次のオブジェクトに移り、"a"を探して、見つかったので(5)と出力する。

withアクションのページに「スコープチェーン」という言葉が出てくる理由は、withを使う事によってスコープチェーンの先頭にオブジェクトを付加することができるからだ。

コード:
1
2
3
4
5
6
7
8
9
10
11
_global.a = 4;
a = 5;
obj = new Object();
obj.a = 6;
with(obj) {
  trace(a); // 6
  delete a;
  trace(a); // 5
  delete a;
  trace(a); // 4
}
出力:

6
5
4

"trace(a);" という同じ命令が3回よばれていて毎回異なった出力になっているのがわかる。これはスコープから連続して変数"a"を削除しているのでFlashが"a"への参照にマッチする変数を探すのにスコープチェーンをより深いところまで探さなければならなかったためである。

 

関数(function)とアクティベーションオブジェクト

関数生成

関数が生成されるとき、Flashがカレントのスコープチェーンとして使っているものがその関数自身のスコープチェーンとして関数に付加される。

一度関数に付加されると、スコープチェーンは変化せず新規オブジェクトはそこに付加されることもそこから削除されることもない。実例を挙げる:

コード:
1
2
3
4
5
6
7
8
9
10
11
a = 5;
test = function() {
  trace(a); // 5
  delete a;
  trace(a); // undefined
};

obj = new Object();
obj.a = 6;
obj.meth = test;
obj.meth();
出力:

5
undefined

"meth"はここではオブジェクト"obj"のメソッドだが、"obj"は"meth"のスコープチェーンの中には「ない」(訳注:つまり、スコープチェーンをたどって"a"を探してもobj.aに行きつく事はない)。これは、"test"のスコープチェーンは"obj"に割り当てられることによる影響を受けないということを意味する。function内部で影響を受けるのは"this"参照(上記の例では使われていない)だけだ。this" 参照については後述。

上記のテストは、"test"というfunctionが元から作られているのでフェアじゃないと感じるかもしれないが、これは問題ではない。ここで唯一重要なのは、"function() {...}"という特定のコード片そのものがコードブロックのなかに存在するということだ。だから一時変数を使わなくても結果は同じになる:

コード:
1
2
3
4
5
6
7
8
9
a = 5;
obj = new Object();
obj.a = 6;
obj.meth = function() {
  trace(a); // 5
  delete a;
  trace(a); // undefined
};
obj.meth();
出力:

5
undefined

 

関数の実行

functionが実行される度に、新規オブジェクトが、ユーザに意識されることなく生成される。このオブジェクトはvarキーワードで生成されるローカル変数、関数パラメータ、arguments(引数)配列の全てを保持している。このオブジェクトはアクティベーションオブジェクトと呼ばれる。「アクティベーションオブジェクト」という言葉もActionScript辞書に一度だけ現れるが、やはりこれもwithの項にある。

functionが実行されると、functionのスコープチェーンはカレントスコープチェーンとして使われ、アクティベーションオブジェクトはその先頭に配置される。よってfunction内部では、スクリプトが実行されると、スコープチェーンは以下のようになる:

アクティベーションオブジェクト -> functionのスコープチェーン

上記のコードでは、functionが_rootの最初のフレームで生成されると、functionが実行されたときに以下のようになる:

アクティベーションオブジェクト -> _root -> _global

 

メモリ管理に関しても、アクティベーションオブジェクトの概念は、functionが実行されるときに実際に何が起こっているかを明らかにする役に立つ:

  1. アクティベーションオブジェクトが生成される
  2. 全てのローカル変数がアクティベーションオブジェクトのプロパティとして生成される
  3. コードがコンテキストオブジェクトとしてのアクティベーションオブジェクトとともに実行される
  4. コードが終ると、プログラム内のどこにもアクティベーションオブジェクトへのリンクがないので、アクティベーションオブジェクトは、保持する全てのプロパティとともに、ガーベッジコレクションされリソースは解放される。

 

ネストした関数とメモリの浪費

導入

やっと面白いところにはいる(待たせてごめんなさい)。Flash MXでは、新たなイベントモデルによって、とても簡単にfunctionを実行時にイベントハンドラ(さらにいえばどんな変数にも)に割り当てることができるようになった。となれば、なんらかのアクションを実行する、もしくは他の関数をイベントハンドラに付加するためのラッパーとしての関数を作成するということが思い浮かぶのが道理だろう。

実例を挙げる:

コード:
1
2
3
4
5
6
7
8
9
resetMC = function(mc) {
  mc._x = mc._y = 0;
  mc.onEnterFrame = function() {
    this._x += 2;
    this._y += 2;
  }
}
resetMC(mc1);
resetMC(mc2);

"resetMC" functionはムービークリップをパラメータとして取得し、そのムービークリップの位置を(0,0)にリセットし、functionをonEnterFrameハンドラに割り当ててムービーが右下に向かって斜めに動き始めるようにする。これは理にかなってはいるが、ここに罠がある。

 

存在し続けるアクティベーションオブジェクト

このまま続けていく前に、いくつか簡単な用語を紹介しなくてはならない。そうでないと、そこで起こっていることの記述が複雑になりすぎる。単純に、functionが他のfunctionの内部で生成されたとき、内側のfunctionを「インナーファンクション」、外側のfunctionを「アウターファンクション」と呼ぶことにする。

もし前に言ったことを全て理解できているなら、もう何が起こっているかわかっているはずだ。上記のコードでは、"resetMC"が呼ばれる度に、現在の実行におけるアクティベーションオブジェクトが生成され、カレントスコープチェーンを形成するためにそのfunctionのスコープチェーンに付加される。3行目に達すると、インナーファンクションが生成されてそれがonEnterFrameのハンドラとして、パラメータとして渡されたムービークリップに対して割り当てられる。

インナーファンクションが生成されると、それ自身のスコープチェーンとしてカレントスコープチェーンが付加される。大事なのは、アウターファンクションのアクティベーションオブジェクトはカレントスコープチェーンの一部だということであり、これは参照が内部のfunctionのスコープチェーンの中に保持されていることを意味する。

一般に、上記のようなコードが_rootの最初のフレームにかかれていた場合、インナーファンクションに付加されているスコープチェーンはこのようになる:

アウターファンクションのアクティベーションオブジェクト -> _root -> _global

後で内側のfunctionが実行されると、スコープチェーンは以下のようになる:

インナーファンクションのアクティベーションオブジェクト -> アウターファンクションのアクティベーションオブジェクト -> _root -> _global

「なにが重要なんだ?」とあなたは尋ねるかもしれない。その答えは、ここではアウターファンクションのアクティベーションオブジェクトがインナーファンクションのスコープチェーンの中で参照され、インナーファンクションがムービークリップのメソッドとして存在し続けることにより、アクティベーションオブジェクト自体も存在し続けるようになるということだ。実際、ガーベッジコレクタにとっては、アクティベーションオブジェクトへの持続的な参照が一つ存在しているのでこれを削除できない。そのせいで、アクティベーションオブジェクトとそれが持ち運んでいる全てのローカル変数がメモリから解放されないのだ。

 

functionの複製

functionの内部でのfunctionの生成が示唆していることがいくつかある。まず第一に、前にも言ったように、新規のアクティベーションオブジェクトはfunctionが実行される毎に生成されている。上記のコードを参照しながら、100個のムービークリップにonEnterFrameを割り当てるために"resetMC" functionを使いたいという状況を想像しよう。そうすると100個の別々のアクティベーションオブジェクトがメモリに残ってしまうことになる。

第二に、アウターファンクション内部でインナーファンクションが生成され割り当てられるので、それぞれのムービークリップに対しても別々のインナーファンクションオブジェクトが割り当てられることになり、それぞれが自身のメモリスロットを占有する。インナーファンクションは、それが付加されている全てのムービークリップにおいて(コードが同じなので)同じことをするのでこれは全く効率的とは言えない。ところでこれは、OOPが、クラスのメソッドを、コンストラクタではなくクラスのprototypeにおいて利用可能することを推奨する理由でもある。メソッドがコンストラクタ自体で生成されると、クラスの各インスタンスに対してfunctionが複製されてしまう。

 

メモリの浪費。メモリリークではない。

この二つを区別することはとても重要だ。メモリリークは、プログラムの使用リソースが継続的に増えていく場合に起こる問題であり、システムクラッシュに至る可能性をもつ。

Flashのネストしたfunctionの場合、起こるのはメモリリークではなく、メモリの浪費である。インナーファンクションはアウターファンクションのアクティベーションオブジェクトへの参照を保持しているが、これは一対一の関係である。インナーファンクションが一度メモリから削除されれば(たとえばインナーファンクションの付加されているオブジェクトが削除されるなど)、そのときはアクティベーションオブジェクトへの一意の参照も削除されガーベッジコレクタはインナーファンクションと、アウターファンクションのアクティベーションオブジェクトの両方を同時に削除することになる(はずである)。よってアウターファンクションのアクティベーションオブジェクトを保持するために使用されるリソースは、無駄に使われてはいるが増加することはなく、したがってメモリの使用が増え続けて制御できなくなるようなことはない。

 

これを全て証明できるだろうか?

アクティベーションオブジェクトの持続性について

そう! こんなコード片を考えてみよう:

コード:
1
2
3
4
5
6
7
8
9
test = function(obj) {
  var a = 5;
  obj.meth = function() {
    trace(a);
  }
}
o = new Object();
test(o);
o.meth(); // 5
出力:

5

"test"というfunctionでは、"a"はローカル変数なので、"test"というfunctionを呼んだ後は"a"が破棄されメモリが解放されることが期待される。ところが、上記のコードを実行すると、"o" というオブジェクト上で"meth"を呼ぶことによって"5"が出力されるが、これは、"a"はかつて"meth"のスコープチェーンで見つかったがまだメモリから解放されていないということを示す。

もしかしたら、インナーファンクション内に"a"への参照がハードコードされているから"a"が見つかったのだと思うかもしれない。Flashは特定の参照を行かしておくために内部的に「何か」をしていたのかもしれない。いい線だが、実際はそうではない。アウターファンクションのアクティベーションオブジェクトはインナーファンクションのスコープチェーンに格納されてい「た」のであり、「全て」のローカル変数が取得可能である。

evalを使えば、スコープチェーン内の変数を動的に検索することができる。以下のコードを参照:

コード:
1
2
3
4
5
6
7
8
9
10
11
12
13
test = function(obj) {
  var aVariable_1 = "Hello";
  var aVariable_2 = "There";
  var aVariable_3 = "Tim";
  obj.retrieve = function(refName) {
    trace(eval(refName));
  }
}
o = new Object();
test(o);
o.retrieve("aVariable_1"); // Hello
o.retrieve("aVariable_2"); // There
o.retrieve("aVariable_3"); // Tim
出力:

Hello
There
Tim

上記のコードでは、"retrieve"というfunctionはどの変数に対してもハードコードされた参照を持っていないが、変数名を渡してevalを使うことで、削除されたと思っていたローカル変数を全て取得することができる。これらの変数はプログラム中で使われないのにメモリには残り、リソースを浪費する。

 

複製とメモリの浪費について

もう一つ。以下のコードを検討してみよう:

コード:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
addFunc = function(obj) {
  var aVariable = new Object();
  aVariable.txt = "Hello there";

  obj.theFunc = function() {
    return aVariable;
  }
}

o1 = new Object();
o2 = new Object();

addFunc(o1);
addFunc(o2);

trace(o1.theFunc().txt); // Hello there
trace(o2.theFunc().txt); // Hello there

trace(o1.theFunc == o2.theFunc) ; // false
trace(o1.theFunc() == o2.theFunc()) ; // false
出力:

Hello there
Hello there
false
false

19行目は、"o1"と"o2"が持つメソッドが同じ名前を持っているのに、メモリ内で同じfunctionオブジェクトを参照していないことを示している。

20行目は、"o1"と"o2"の両方にある"theFunc"が返すオブジェクトが、同じ"txt"というプロパティに同じ値を持っているのに、これらのオブジェクト自体は別々のものであり、つまり"hello there"という文字列が各オブジェクトに対して一つずつ、合わせて二つメモリに格納されているということを示している。

実は、オブジェクトに"theFunc"メソッドを付加するために"addFunc"が呼び出されると、その度に"Hello there"という文字列がメモリ内で複製されている。

 

それではどうするべきだろうか?

上で示されたようなネストしたfunctionの仕様に格別興味がないというのでない限り、functionは全て同じlevelに作成し、ネストしたfunctionの代わりにfunction内部のfunction参照だけを使うのがベストだろう。

例えば、上記のコードを書き直すなら、このようになるだろう:

コード:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
theFunc = function() {
  return aVariable;
}
addFunc = function(obj) {
  var aVariable = new Object();
  aVariable.txt = "Hello there";

  obj.theFunc = theFunc;
}

o1 = new Object();
o2 = new Object();

addFunc(o1);
addFunc(o2);

trace(o1.theFunc().txt); // undefined
trace(o2.theFunc().txt); // undefined

trace(o1.theFunc == o2.theFunc) ; // true
出力:

undefined
undefined
true

上記の通り、今度は17行目と18行目のtrace文の出力がundefinedになる。これは、"theFunc"が"o1"と"o2"で呼び出された時にFlashが"aVariable"という参照をスコープチェーンで見つけられない、つまり"addFunc"のアクティベーションオブジェクトがこのメソッドのスコープチェーンに追加されておらずローカル変数である"aVariable"が期待通りに削除されているということである。

加えて、20行目は"true"を出力しており、これは"o1"と"o2"両方のfunctionオブジェクトが同一でありメモリ内で一つのスロットしか使っていないということを意味する。

メモリスペース使用の改良に加えて、このアプローチをとることでコードがより速くなるだろう。基本的には、メモリ内で新規に関数を生成するには時間がかかる:メモリ割り当ての時間、データ転送の時間、等々。よってネストした関数を使うときは、外側の関数を呼ぶ度に、内側の関数を構築するのにいくらかのCPUサイクルを消費している。functionを前もって作成しておき外側の関数で単純な割り当てを行うだけなら、動作がかなり高速になるだろう。

 

結論

ここでスコープチェーンとメモリの浪費について結論を出したい。要約すると、ここまでで議論してきたことのキーポイントは以下のようになる:

  1. スコープチェーンはFlashが変数を探す際に調べるオブジェクトの連なりである。

  2. functionが生成されるとき、カレントスコープチェーンがそこに付加される。

  3. functionが生成される度に、アクティベーションオブジェクトと呼ばれる新規オブジェクトが生成されて全てのローカル変数を保持する。このオブジェクトはそのfunctionに付加されたスコープチェーンの先頭に置かれる。

  4. 関数をネストさせると、アウターファンクションのアクティベーションオブジェクトはインナーファンクションのスコープチェーンの中に置かれる。

  5. インナーファンクションが存在し続けるオブジェクトに対してメソッドとして付加される、もしくはアウターファンクションから"return"される場合、インナーファンクションはアウターファンクションのアクティベーションオブジェクトと共に存在し続けることになる。これはメモリの浪費につながる。

  6. メモリの浪費を避けるためには、最も簡単な解決法として、ネストしたfunctionを使わず代わりに外部に関数を作成し、参照だけを付加するようにする。

 

実際、これはかなり単純なことだ。問題は、これが全く自明なことというわけではないことだ。あなたはもしかしたら何度も知らずにリソースを浪費していたかもしれない。

びっくりして、ネストしたfunctionのあるファイル全てを修正しにかかろうと考える前に、ほとんどの場合ではこれらのことはおそらくあまり問題にならないと言っておきたい。小さなプロジェクトで、マシンの全てのリソースを食い尽くしているわけでもなくプロジェクトがそのままでうまく行っているのなら、別に心配することはない。ここで指摘していることは、おそらく巨大なプロジェクトで問題になることであり、その場合自分のスクリプトの裏でFlashが何をしているかを知っておくことはいいことだと思う

最も大きなメモリの浪費が起こるのは、大きなテキストをネストしたfunctionで扱う場合だと思われる。文字列はすぐに数十〜数百文字に達してしまうので、文字列をローカル変数として使っていてそれが持続的に存在することになるのであれば、それは実際に問題になるかもしれない。整数や参照が数個あるだけなら、特に問題はない。

わざわざ改めていうまでもないが、私の考えとしては、ファイルサイズやメモリ使用量や効率といった点において無駄がなければないほどよい。だから、ActionScriptでコーディングするときはこういうことを心に留めておこう :)

ここまで読んでくれてありがとう。このドキュメントが役に立つことを願っている。なにか間違い(コード、事実、用語、文法、スペル、等々)をみつけた、あるいは起こっていることの内容を説明するのにもっと適切な別の例があると思ったら、またあるいはなにか新しい事実を教えてくれるなら、気軽にメールして欲しい。フィードバックをもらえるならこれ以上の喜びはない。

もう一つ、終りにする前に、このトピックに付随していくつか質問が提起されるだろうと思ったので、以下に"Extras"というセクションを追加した。

最後に:機能は常に多くの実践の基盤になる。ネストした関数はメモリの浪費につながるが、これはアプリケーションによっては許容範囲のコストである。例えば、スコープチェーンの機能を使えばprivateプロパティとstaticプロパティが実装できる:詳しくは別の記事で紹介する。

 

参考

ここにあるもののほとんどはFlashCoders Listの様々なスレッドから直接持ってきている。最も興味深いスレッドは以下のものだ:

http://chattyfig.figleaf.com/ezmlm/ezmlm-cgi?1:sss:56601:200212:blejmgjoemfcdojimbmn#b

 

Extras - 質疑応答

スコープチェーンはどこまで深くできる?

えーと、わかりません :) 以下のコードで5段までfunctionをネストさせているが、全てのアクティベーションオブジェクトが持続的に存在し続ける。

コード:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
a1 = 5;
addFunc = function(obj) {
  var a2 = 6;
  var func = function(obj) {
    var a3 = 7;
    var func = function(obj) {
      var a4 = 8;
      var func = function(obj) {
        var a5 = 9;
        obj.retrieve = function(refName) {
          var a6 = 10;
          trace(eval(refName));
        }
      }
      func(obj);
    }
    func(obj);
  }
  func(obj);
}
o = {};
addFunc(o);
o.retrieve("a6"); // 10
o.retrieve("a5"); // 9
o.retrieve("a4"); // 8
o.retrieve("a3"); // 7
o.retrieve("a2"); // 6
o.retrieve("a1"); // 5
出力:

10
9
8
7
6
5

全ての変数を取得できているので、各functionのアクティベーションオブジェクトが最も内側のインナーファンクションのスコープチェーン内に保持されているということになる。

よって実際のところ、スコープチェーンをどれだけ深くできるかはわからないが、たぶんかなり深くまで可能だろう。いずれにせよ、上記のコードは既に5段もネストしたfunctionを持っており、もし実際のプロジェクトでコードがこんな感じになっていたとしたら、コーディングの方針を真剣に考え直すべきだと思う :) !

 

スコープチェーンとprototypeチェーン

Flashが変数を探すときFlashはスコープチェーンを通じて検索を行うと前に指摘した。これは正しいがそれに加えてもう一つFlashが変数を探し出すメカニズムがある。このメカニズムとは継承チェーンであり、prototypeチェーンとしても知られている。この記事はOOPについての議論ではないので、深く追求せずRobin Debreuilのオンラインブックを紹介するだけにとどめる。このサイトはFlashでのOOPがどのような働きをもつかを理解するのにもっとも優れたリソースである。

一言で言うと、Flashにおける全てのオブジェクトはクラスのインスタンスであり、クラスは他のクラスから継承されるといったようなことだ。それぞれのクラスは自分自身のプロパティとメソッドのセットを持っており、オブジェクトのメソッドを呼び出すと、メソッドがそのオブジェクト自体の中に見つからなければ、Flashは継承チェーンをたどって、要求されたメソッドがより上のほうで見つかるかどうか調べる。

スコープチェーンは内部的に扱われるが、prototypeチェーンは開発者が利用することができ、prototypeチェーンにおけるオブジェクト間のリンクは"__proto__"という参照の形で存在する。スコープチェーン内の検索が行われるとき、prototypeチェーンの検索はスコープチェーン内の各オブジェクトにおいて行われる。

以下のコードで実際に提示する:

コード:
1
2
3
4
5
6
7
8
9
10
11
a = 5;
addFunc = function(obj) {
  var __proto__ = new Object();
  __proto__.a = 6;
  obj.meth = function() {
    trace(a);
  }
}
o = {};
addFunc(o);
o.meth(); // 6
出力:

6

"6"という値は、"a"に対して"addFunc"というfunction内のローカル変数として割り当てられているのではなく、Flashが"a"を探して取得されたものだ。ここでFlashが何をしたのか、いつ"meth"が実行されたのかを一歩一歩みていきたい。

まず最初に、Flashは"meth"のアクティベーションオブジェクト内で"a"を探すがここでは見つからない。次にFlashは"meth"のアクティベーションオブジェクトが"__proto__"という参照を持っているかどうかをチェックする。ここにはないので(あるいは見つからないので)、先に進んでスコープチェーン内の次のオブジェクト("addFunc"のアクティベーションオブジェクト)を調べる。"a"はここでは見つからない。Flashはこのオブジェクトの"__proto__"参照を調べて、ここで見つかった。Flashは"__proto__"参照によって指し示されたオブジェクトの中で"a"を探してそこで見つけたのだ! そして最後にFlashは値を表示する。

 

変数は明示的なスコープ無しにどうやって割り当てられるのか?

一番最初に示しておくべきだったかもしれない。これも、複雑な振る舞いではないが、自明とは言えない。割当を行うとき、例えば"myVar = 5;"のようにするとき、,"myVar"への参照を取得するためにスコープチェーン内の各オブジェクトが、_globalを除いて、調べられる。もしこれらのオブジェクト中の一つ(仮に"o"とする)において"myVar"への参照が見つかったら、"o"において割り当てが行われる。

"myVar"への参照が見つからなければ、そのときは新たな参照"myVar"がスコープチェーンの末尾に生成される。これは_globalのすぐ上のオブジェクトだ。

どうやら割り当てにおいては、スコープチェーン内のオブジェクトのprototypeチェーンはチェックされない。よって"myVar"への参照が"o"のprototypeチェーン内に存在したとしても、それがスコープチェーンにおいて_globalより上位のもので最後のオブジェクト(訳注:つまりムービークリップ)でない限り"o"での割り当ては行われない。

_globalオブジェクトのプロパティを作成もしくは値を設定するためには、_globalを明示的に指定する必要がある。

このことは以下のテスト用スクリプトで説明できる:

コード:
1
2
3
4
5
6
7
8
9
10
11
o1 = {};
o2 = {};
with (o1) {
  with (o2) {
    trace("The first 'a' reference found is: " + a);
    a = 5;
  }
}
trace(o1.a); // undefined
trace(o2.a); // undefined
trace(a); // 5
出力:

The first 'a' reference found is:
undefined
undefined
5

 

コード:
1
2
3
4
5
6
7
8
9
10
11
o1 = {a:4};
o2 = {};
with (o1) {
  with (o2) {
    trace("The first 'a' reference found is: " + a);
    a = 5;
  }
}
trace(o1.a); // 5
trace(o2.a); // undefined
trace(a); // undefined
出力:

The first 'a' reference found is: 4
5
undefined
undefined

 

コード:
1
2
3
4
5
6
7
8
9
10
11
12
o1 = {};
o1.__proto__ = {a:4}
o2 = {};
with (o1) {
  with (o2) {
    trace("The first 'a' reference found is: " + a);
    a = 5;
  }
}
trace(o1.a); // 4
trace(o2.a); // undefined
trace(a); // 5
出力:

The first 'a' reference found is: 4
4
undefined
5

 

withとスコープチェーン

私は"with"を愛しており且つ"with"を憎んでいる。withはスコープチェーンと相互作用するので、ネストしたfunctionと多くの類似点を持っている。例えば明示的なスコープなしで行われる割り当てについて上で論じたことは、ネストしたfunctionでも"with"でも当てはまる。しかし、いくつかの違いもある。ここで示したい主要な違いは、functionの生成とつながっている。カレントスコープチェーンはfunctionが生成されたときにそのfunctionに付加されるとこの記事の上のほうで指摘した。これまで見てきたように、これはアクティベーションオブジェクトの持続をもたらす。カレントスコープチェーンがfunctionの生成後に付加されるなら、"with"を使って任意のオブジェクトをそのfunctionのスコープチェーン内に追加できるということになる。しかし、このようには動作しない。このコードをみてほしい:

コード:
1
2
3
4
5
6
7
8
9
o1 = {a:5};
o2 = {};
with (o1) {
  trace(a); // 5
  o2.aMethod = function() {
    trace(a);
  };
}
o2.aMethod(); // undefined
出力:

5
undefined

上記のコードでは、"o1"というオブジェクトをスコープチェーンの先頭に置くために"with"を使用した。4行目は、"a"を正しく取得できることを示している。カレントスコープチェーンにおいて、新規のfunction(5行目)を生成しそれをo2に新規メソッドとして割り当てる。その後で"o2.aMethod();"を呼び出したときにはundefinedが返ってくる。これは"a"がスコープチェーン内で見つからなかったということであり、それはo1というオブジェクトがfunctionのスコープチェーンにに追加されなかったということである。

"with"を使って手動でスコープチェーンに置かれたオブジェクトはfunction生成におけるカレントスコープチェーンの一部とは見なされないのだ。

 

"this"参照はどうか?

自分でテストしたかぎりでは、その深さに関わらず"this"参照はスコープチェーンによって影響を受けない。"this"はfunctionを呼び出す元のオブジェクトを常に参照している。

別の言い方をすれば、functionのスコープチェーンが決して変化しないならfunction内部の"this"の意味は、どのオブジェクトからfunctionが呼び出されたかによって変化することはない。一番最初の例にもどると:

コード:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
a = 5;
test = function() {
  trace(this.a);
};

obj = new Object();
obj.a = 6;
obj.meth = test;

obj2 = new Object();
obj2.a = 7;
obj2.meth = test;

obj.meth(); // 6
obj2.meth(); // 7
出力:

6
7

"this"について言うと、"常にその関数が呼び出される元のオブジェクトを参照している"という点で非常に興味深いプロパティである。前にローカル変数はアクティベーションオブジェクトのプロパティであると指摘した。varを使ってアクティベーションオブジェクトにfunctionを付加しアクティベーションオブジェクトの内部からfuncitonを実行するなら、このfunctionの"this"参照はアクティベーションオブジェクトそれ自体を参照することになる! この事実により、ある特定のアクティベーションオブジェクトへの参照を引き出して、どこかに格納するかあるいは配列演算子を使って取得するかあるいはアクティベーションオブジェクト内部にプロパティを動的に設定するといったことが可能になる(これもまた、使い道があればの話だが)。

コード:
1
2
3
4
5
6
7
8
9
a = 4;
test = function() {
  var a = 5;
  var getRef = function() {
    trace(this.a);
  }
  getRef(); // 5
}
test();
出力:

5

 

 

配列演算子とeval

ActionScriptのevalはもう推奨されないのかどうか、evalと配列演算子の違いは何なのか、みんな知りたがる。Ralf Bokelbergのこの投稿からの引用をもってこれらの質問に対する答えに代えたい。引用は以下にある:

<quote>
evalのマントラを知っているだろうか:

evalは推奨されないわけではない
evalは役立たずではない
evalはとても便利だ

文字列として与えられたオブジェクトにアクセスするのに、なにか他にやり方があるだろうか?

obj = {subobj: {prop: 666}}
path = "obj.subobj.prop";
trace(this[path]); //undefined
trace(eval(path)); //666

bokel
</quote>

そう、そのとおり、主要な違いの一つは、配列演算子がオブジェクトの直接的なプロパティしか取得できないのに対してevalはパスを解決できるということだ。

上で論じられていることから、配列演算子とevalの間の違いを言い表す別の方法は、スコープチェーンとprototypeチェーンに関係したものである。ある単一の変数(パスではない)を取得するためにevalもしくは配列演算子を使っていると仮定すれば、このように言うことができる:配列演算子は、与えられたオブジェクトのprototypeチェーンを調べて変数(プロパティ)を取得するために使われる。evalはカレントスコープチェーンを調べて変数を取得するために使われる。そして、上で指摘したように、スコープチェーンを調べるということは、スコープチェーン内の各オブジェクトのprototypeチェーンを調べるということを含んでいる。

 

全部理解できただろうか?

よろしい、これで最後だ。上で論じたこと全てを用いて、Flash MXを使わずに、以下のコードの出力内容を当てることができるだろうか?

コード:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
getMethod = function() {
  var setProto = function() {
    this.__proto__ = o1;
  };
  setProto();

  return function() {
    trace(a);
  }
}

_global.a = 4;
o1 = {a:5};
o2 = {a:6};
a = 7;

o2.theMethod = getMethod();
o2.theMethod();
  1. 4
  2. 5
  3. 6
  4. 7

元記事のURLはhttp://www.timotheegroleau.com/Flash/articles/scope_chain.htmです。
日本語訳は、原著者であるTimothee Groleau氏の許可を得てFACEs Projectが行いました。