このブログの更新は Twitterアカウント @m_hiyama で通知されます。
Follow @m_hiyama

メールでのご連絡は hiyama{at}chimaira{dot}org まで。

はじめてのメールはスパムと判定されることがあります。最初は、信頼されているドメインから差し障りのない文面を送っていただけると、スパムと判定されにくいと思います。

参照用 記事

プログラマのためのJavaScript (6):関数オブジェクトの秘密

JavaScriptで「面白いな」と感じることのひとつは、あらゆることが“オブジェクト構造に対して/オブジェクト構造の上で”行われることです。関数の概念そのもの、関数のコンパイルと実行のメカニズムもオブジェクト構造と深く結びついています。

今回の内容:

  1. JavaScriptの実行モデル:復習と補足
  2. 関数オブジェクト
  3. 関数のコンパイル
  4. 関数の実行
  5. 今回のまとめ(+雑感)

JavaScriptの実行モデル:復習と補足

前回説明したように、JavaScript処理系は「コンパイラJavaScript仮想機械(JSVM)、オブジェクト構造」からなります(そう考えるといい、ということ)。このなかで、オブジェクト構造(オブジェクト包含階層、オブジェクト空間)は、処理系と同じ寿命を持ち、処理系の稼働中に変化していきます。JavaScriptプログラムの走行による影響は、すべてオブジェクト構造に記録されるのです。

JavaScriptのすべてのオブジェクトは、大域オブジェクトを基点として、プロパティをたどってアクセスできます。ただし、プロパティのなかにはユーザー(JavaScriptプログラマ)に解放されてないものもあるので、ユーザーからすべてのオブジェクトが見えているわけではありません。が、処理系は全オブジェクトを完全に把握しています(そうでないと困りますよね)。

●関数オブジェクト

JavaScriptでは関数もオブジェクトです。つまり、プロパティの集まりですね。例えば、定義時の引数個数は、arity(またはlength)というプロパティに保存されています。関数のソースも文字列で持っているようですが、対応するプロパティは解放されてません、でもメソッドtoStringでソースにアクセスできます。


js> function one() {return 1;}
js> var s = one.toString()
js> print(s)

function one() {
return 1;
}

js>

さて、関数オブジェクトの最も重要な(しかし解放されていない隠れた)プロパティは、コード実体を参照するプロパティでしょう。仮にこのプロパティを#code#という特殊な名前で表すとします。func.#code#は、関数funcのコード実体であり、コンパイル済みJSVMコード列、またはネイティブ(機械語)コード列です。

「関数を呼び出す」とは、適当なセットアップの後で、関数オブジェクトの#code#プロパティが指しているコード列をJSVM(またはネイティブ実行系)で実行することです。

●関数のコンパイル

次のようなJavaScriptプログラム(コンパイル単位)をコンパイルすることを考えましょう。


<script>
// トップレベルコード1
function func1() {
// 関数コード1
}
// トップレベルコード2
function func2() {
// 関数コード2
}
</script>

コンパイラは、3本のJSVMコード列を生成します。‘トップレベルコード1’と‘トップレベルコード2’が連結されて、1本のトップレベルJSVMコード列になり、コンパイル後ただちにJSVMで実行されます。

もちろん、func1とfunc2もトップレベルと共にコンパイルします。その手順は次のとおり:

  1. ‘関数コード1’に対応するJSVMコード列を生成する。
  2. ヒープに関数オブジェクトをアロケートして、関数オブジェクトの#code#プロパティにコンパイル済みコード(への参照)をセットする。
  3. 大域オブジェクトに"func1"という名前のプロパティを作り、今作った関数オブジェクトをその値とする。

func2に関しても同様です。

まとめて言えば、「関数宣言文が含まれるプログラムの場合、コンパイル時に関数オブジェクトが生成されて、大域オブジェクトのプロパティとしてセットされる」となります。コンパイラの作業はオブジェクト構造を変更することになるので、コンパイラは広義のインタプリタ(言語処理系全体)の一部とみなすのが自然です。(コンパイルフェーズだけを切り離したバッチコンパイル方式も可能ですが。)

トップレベルコードをcompile-and-goした後で捨ててもよいのは、関数コードとは違って、大域オブジェクトからトップレベルコードへアクセスする道筋がないからです。大域オブジェクトからたどれないデータはどうせゴミ集めで回収される運命です。

●関数の実行

関数はコンパイルされ、適当な名前でアクセスできるオブジェクトになります。しかし、その関数を呼び出すコードがなければ実行されません。にもかかわらず、ブラウザ向けJavaScripでは、どこからも呼び出されてない関数が定義されることがあります。例えば:


function handler(e) {
e = (e? e : event);
var cc = (e.charCode ? e.charCode :
(e.which ? e.which : e.keyCode));
alert("You pressed " + cc);
return true;
}

ブラウザ環境の場合には、ユーザーが書いたプログラムから明示的に呼び出される以外に、関数は環境から呼び出されます。上の例のhandlerが、HTMLのinput要素に次のようにアタッチされていれば、ブラウザのキーボードイベントにより呼び出されます。


input.onkeypress = handler;

ブラウザ向けプログラムでは、(傾向として)環境から呼び出されるコールバック一式を準備するコーディングになります。

●今回のまとめ(+雑感)

  1. 関数オブジェクトは、コード実体を指す隠れたプロパティを持つ。
  2. コンパイラは、トップレベルコードを1つにまとめてコンパイル済みコード列を生成する。
  3. 関数定義1つに対して、1つのコンパイル済みコード列が生成され、対応する関数オブジェクトと大域プロパティもコンパイラにより作られる。
  4. 関数は他のコードから呼び出される以外に、環境からイベントハンドラとしてコールバックされることがある。

こうして説明してみると、(構文はともかく)実行メカニズムはLisp系言語と似ているなーと思います。JavaScriptは、car/cdrを持ったconsセルの代わりに任意の名前を持った動的レコードを基本データ構造に採用した、Lispの方言という感じです。データリテラルによる表現力がけっこう強力(例:JSON)なのは、リテラルがほぼS式に対応するからでしょう。

まだ触れてない関数関連の面白い話題(入れ子の関数、Functionコンストラクタ、applyメソッドなど)がありますが、それは後回しにして、次回はプロトタイプの説明をするつもりです。