ログインしてさらにmixiを楽しもう

コメントを投稿して情報交換!
更新通知を受け取って、最新情報をゲット!

手作りネットプロトコル工房コミュのJavaScriptのスコープ

  • mixiチェック
  • このエントリーをはてなブックマークに追加
JavaScriptのスコープに関する話題は、非常に深いものがあるので、今度きちんと書いてみようと思います。


(12/14追記) Rhino の未翻訳ドキュメントですが、スコープに関してすごい重要な事が書いてあるので、自力で翻訳してみました。mozilla-japan に投稿しようかとも思ったんですが、投稿の仕方がわからないので、そのままお蔵入りしてしまった奴です。

どうせ、誰も読まないでしょうが...。


( ... と誰かが返事をしてくれる事を期待して挑発的な発言をしてみる(^-^;)


元記事
http://www.mozilla.org/rhino/scopes.html


Rhinoをマルチスレッド環境で利用する際、「コンテクスト」と「スコープ」の違いについて理解することは大切です。 コンテクストもスコープもスクリプトを実行するときに必要だということには変わりはありませんが、役割が異なります。 単にRhinoを埋め込むだけの場合はひょっとしたらここに書いてあることを理解する必要はないかもしれません。 ですが、複雑な機能を利用する埋め込みの場合、ここに書いてあるテクニックが役に立ちます。

コンテクストとは
Rhinoの コンテクストオブジェクトは実行環境に関する情報の中で、スレッドを利用する際に特徴的な情報を保存する役割を担っています。 それぞれのスレッドに対して必ずそれぞれ一つのContextオブジェクトが対応し、それがJavaScriptの実行を行います。

現在実行中のスレッドに対するコンテクスト、Contextオブジェクトのインスタンスを取得するには、 Contextクラスのenter()メソッドを呼び出します。

Context cx = Context.enter();

また、処理が終わりましたら、単に Contextクラスの exit() メソッドを呼び出します。

Context.exit();

一つのスレッドが、enter()メソッドを複数回呼び出しても問題ありません。 割り当て済みのContext インスタンスが返され、参照カウントが加算されます。 参照カウントが0に達した場合のみ、現在のスレッドに対応付けられたContextオブジェクトインスタンスが削除されます。

exit()メソッドの呼び出しを忘れない様に注意して下さい。 exit()メソッドは、例外が発生した場合に備えて、必ずfinallyブロックの中で呼び出す必要があります。

スコープ
ある一つのスコープは、複数のJavaScriptオブジェクトからなります。 スクリプトを実行するためには、標準ライブラリオブジェクトのFunctionとObjectトを格納する場所と同様に、トップレベルスクリプト変数を格納するの為のスコープが必要となります。

スコープがコンテクストから独立しているということを知ることはとても大切です。 あるコンテクストを使ってあたらしくスコープを生成し、スクリプトを実行/評価することが出来ます。 その後でコンテクストがexit()メソッドを呼び出すことで終了し、他のコンテクストに入ったとしても、他のスレッドによって実行されたとしても、問題ありません。 それだけではなく、スクリプトを複数のスレッドから同時に同じスコープ上で実行することすら可能です。 Rhinoは、JavaScriptオブジェクトのプロパティーに対して複数のスレッドから同時にアクセスした場合に一意にアクセス出来ることを保障しますが、それ以上の保障は行いません。 もし2つのスクリプトが同じスコープを共有する場合は、それぞれのスクリプトは競合しないように、責任を持って協調的にアクセスする必要があります。

最上位スコープは、 標準ライブラリオブジェクトを初期化する Context.initStandardObjects()の呼び出し時に作成されます。

ScriptableObject scope = cx.initStandardObjects();

アプリケーションにRhinoの組み込む一番簡単な方法は、必要になるたびにいちいち新しいスコープを作成することです。 しかし、 initStandardObjects は 非常に時間のかかる処理ですし、メモリもかなり消費します。 以下に、複数のスコープ/スレッドから共有する方法を見ていきましょう。

名前解決
では、識別子はどのようにスコープ中から検索されるのでしょうか。 通常、これは、現在の変数オブジェクト(これはプログラム中のどのコードが実行されているかとは関係がありません) から検索が始められ、prototypeチェーンの追跡、parentチェーン追跡と続きます。 以下のダイアグラムでは、6つのオブジェクトが追跡されるが順が描き出されています。


深さ=2のスコープがプロトタイプと共に検索される順番

さらに具体的な例を見ていきましょう。 次のスクリプトを実行するところを考えてみます。

var g = 7;
function f(a) {
var v = 8;
x = v + a;
}
f(6);

まず最上位変数(グローバル変数) g があり、fの呼び出しは 新たな最上位変数 x を作成します。 全ての最上位変数は そのスコープオブジェクトのプロパティーになっています。 関数fの実行を開始するとき、スコープチェーンは関数の一時変数スコープから始まり、最上位変数のスコープで終わります。 関数の一時変数スコープは2つのプロパティーを持っています。 一つは 引数としての a で、もう一つは、一時変数としてのvです。 最上位変数スコープは 変数gと関数fを保持しています。



x = v + a; が実行されるとき、まずプロパティー x を探します。 もし見つからなかった場合は、新しいプロパティー x が最上位変数スコープで作成されます。
スコープの共用
JavaScriptは、伝統的なクラスによる継承ではなく、むしろ 委譲という概念を利用する言語です。 これはある意味それだけで非常に大きなトピックなのですが、見方を変えると、複数のスコープからいくつかの読み取り専用変数を共有する方法を理解するために重要な示唆を与えてくれます。 共有を実現は、オブジェクトにprototype をセットすることで実現できます。 JavaScriptでは オブジェクトのプロパティーにアクセスしようとすると、そのオブジェクトはまずその与えられた名前で探そうとします。 見つからない場合、次にそのオブジェクトのprototypeから探します。 これはプロパティが見つかるか、prototypeのチェーンが尽きるまで繰り返されます。

ですから、複数のスコープに渡って情報を共有するためには、最初に共有したいオブジェクトをつくります。 ほとんどの場合、このオブジェクトは initStandardObjects() によって作成されたものを利用します。 また、そのオブジェクトは、埋め込むプログラムの用件に合わせていくつかのオブジェクトが追加されるかもしれません。 次に、新しいオブジェクトを作成して、そのオブジェクトのsetPrototype()メソッドを利用してprototypeを共有オブジェクトに設定し、parentをnull に設定します。

Scriptable newScope = cx.newObject(sharedScope);
newScope.setPrototype(sharedScope);
newScope.setParentScope(null);

newObject()の呼び出しは単にプロパティを持たないJavaScriptオブジェクトを作成します。 渡された sharedScope は、標準ライブラリ Objectの prototype を初期化するために利用されます。

これでnewScopeを使ってスクリプトを実行できます。 このスコープのことをインスタンススコープと呼ぶことにしましょう。 スクリプトで定義された全ての最上位関数/変数は最後までインスタンススコープのプロパティーとして定義されます。 Function,String,RegExpといった標準ライブラリオブジェクトは共有スコープのなかから見つかることになります。 複数のインスタンススコープを定義できます。 また、独自の変数を共有することも出来ます。 複数のインスタンススコープはマルチスレッド環境下でも利用可能です。

封印済み共有スコープ

ECMAScript標準では、スクリプトは全ての標準ライブラリオブジェクトに(外部から)プロパティを追加することが出来るとの旨がの定義されています。 これは、多くの場合、同様に標準ライブラリオブジェクトのプロパティーを変更・削除も出来てしまうことも意味しています。 このような動作は、共有スコープを実現するに当たっては望ましい動作ではないかもしれません。 何故ならば、スクリプトが誤って共有スコープから標準ライブラリオブジェクトのインスタンスにプロパティーを追加してしまった場合、そのオブジェクトはガベージコレクトの対象となることが出来ないため、潜在的にメモリリークの原因となってしまうからです。 また、もしあるスクリプトが標準ライブラリオブジェクトのプロパティを変更してしまった場合、そのライブラリを利用しているほかのスクリプト全てが正しく動作しなくなってしまうということもあります。 そのようなバグは非常にデバッグが困難で簡単には取り除くことが出来ません。 そのような事態が発生してしまう危険性を取り除くために、封印と呼ばれる機能を 共有スコープと全ての標準ライブラリオブジェクトに適用することが出来ます。

封印済みオブジェクトという概念はRhino特有のJavaScriptに対する機能拡張です。 封印済みオブジェクトは、外部からプロパティーを追加削除変更することが出来ません。 変更しようとすると例外が発生します。 全ての標準ライブラリ中のオブジェクトを封印済みにする場合、 Context.initStandardObjects(Scriptable Object,boolean) の呼び出し時 引数sealedにtrueを渡してください。



この処理を行うと、封印は標準ライブラリオブジェクトだけにとって有効となりますが、共有スコープそれ自身は封印されません。 ですからinitStandardObjectsを呼び出しした後に sealedSharedScopeにアプリケーション特有のオブジェクトや関数を組み込むことが出来ます。 組故意が完了したら ScriptableObject.sealObject() を呼び出して封印処理を行いましょう。



現在のところ、封印済み共有スコープにプロパティを追加した場合は明示的にその追加したプロパティに封印処理する必要があります。 何故ならば、 sealedSharedScope.sealObject(); を呼び出した後はそのプロパティに異なる値をセットすることは出来なくなりますが、そこにセットされているオブジェクト自身を変更することは依然として可能だからです。

上記の初期化方法には一つ問題があります。 静的スコープから JavaScript関数を呼び出した場合、まず関数の中から変数を探し出しだそうとしますが、見つからなかった場合次にクロージャ(lexicall enclosing scope)から探し出そうとします。 このことは実は後で問題を引き起こすことがあります。 もしも共有スコープ内に関数を作成するとき、その関数が インスタンススコープに存在する変数にアクセスする必要がある場合、問題となります。

Rhino 1.6では、ダイナミックスコープという機能を利用することが出来ます。 ダイナミックスコープを使うと、関数は最上位変数スコープをクロージャよりも優先して変数を検索します。 これにより、共有スコープ内にある関数であっても、インスタンススコープ内の情報にアクセスすることが出来ます。


サンプル DynamicScopeは 上記の論点を全て描き出しています。

スコープにまつわるそのほかの事柄
スコープをセットアップするときに解決すべき事柄には次の事柄が挙げられます。
(1) スクリプトが未定義の変数に値を割り当てようとしたときグローバル変数はどのスコープに保存されるべきか。 そして、
(2) スクリプトが変数を参照するとき、どの変数に対してアクセスするべきか。

(1)に対する答えは、最上位親スコープ(the ultimate parent scope)にあります。 Rhinoは parentチェーンを追跡して最終的には最上位まで到達します。 そして、そこに変数を配置します。 問題(2)は、問題(1)のparentチェーンとはまた別に、さらに検索するべきスコープが存在することを示唆しています。 それらのスコープはprototypeスコープとして追加することが可能です。 Rhinoが変数を検索するとき、まず現在のスコープから検索し、prototypeチェーンを検索後、parentチェーンとそれぞれのオブジェクトのprototypeチェーンを、最上位parentスコープに到達するまで続けます。

コメント(28)

ログインすると、みんなのコメントがもっと見れるよ

mixiユーザー
ログインしてコメントしよう!

手作りネットプロトコル工房 更新情報

手作りネットプロトコル工房のメンバーはこんなコミュニティにも参加しています

星印の数は、共通して参加しているメンバーが多いほど増えます。

人気コミュニティランキング