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

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

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

参照用 記事

プログラマのためのJavaScript (5):コンパイル単位

最近の言語処理系では、コンパイラインタプリタの区別がしにくくなっています。対話的処理系でも、内部ではコンパイラが走っていることが多いからです。JavaScriptも、「コンパイラインタプリタか?」を判断しにくい実行形態ですね。しかし、コンパイラとして、あるいはインタプリタとしてのJavaScript処理方式を理解してないと、プログラミングで戸惑<とまど>うこともあるので、今回はこの点をハッキリさせましょう。

JavaScriptの典型的な実行モデル

現実の実装は色々でしょうが、ここでは、JavaScriptは次のように実行されると考えましょう

通常、コンパイラの存在を意識しなくて済むのは、コンパイル後ただちに(今まさにコンパイルされたコードが)実行されるからです。このような方式を、compile-and-go方式といいます。

上で、オブジェクト構造といったものは、処理系が管理している全オブジェト群で、オブジェクトモデル、オブジェクト包含階層(object containment hierarchy; OCH)、オブジェクト空間、オブジェクトシステムなどとも呼びます。これは、前回説明したように、大域オブジェクトをハブ(ルート、ベース)ノードとする“辺ラベル付き有向グラフ”(edge-labelled directed graph)になっています。

コンパイル単位

コンパイル単位とは、コンパイラが一度にコンパイルするストリームのことです。

Javaでは、1個のソースファイルがコンパイル単位です。Cでは(C++でも)、#includeがあるので、ソースファイルがコンパイル単位とは限りません。#includeを(再帰的に)全部処理して作られたストリームがコンパイル単位ですね。通常、#include処理はC前処理系cppが行うので、その結果(つまりCのコンパイル単位)がパイプや一時ファイルを介してCコンパイラ本体に渡されるのです。

さて、JavaScriptの場合ですが、ブラウザ環境では、script要素の中身がコンパイル単位です。src属性で外部ファイルを指しているときは、その外部ファイルがコンパイル単位となります。例えば、次の例には3つのコンパイル単位が含まれます。


<html>
<head>
<title>3つのスクリプト</title>
<script id="script1" src="outer.js"></script>
<script id="script2">
//
// ここにスクリプトが記述されている
//
</script>
<head>
<body>
<script id="script2">
//
// ここにスクリプトが記述されている
//
</script>
</body>
</html>

この例では、HTMLファイルの解析中に、ブラウザによりJavaScriptコンパイラが3回呼び出されます。実行はcompile-and-go方式なので、コンパイル実行後ただちにJSVMも走ります。JSVMによる実行が済めば、コンパイル結果のコード列(正確に言えばトップレベルのコード列)は捨ててもかまいません。

●compile-and-goの確認

JavaScriptがcompile-and-go方式であることは次の例で確認できます。


<script id="script1">
alert("This is script1: one=" + one());
function one() {return 1;}
</script>

もし、ソースを1行ずつ実行するなら、1行目のalertで関数oneを使用することはできません。

次の例では、関数oneの定義に構文エラーがあります(括弧が付いてない)。script1のコンパイルが失敗するので、script1はまったく実行されないのです。その結果、script2だけが実行されます。


<script id="script1">
alert("This is script1: one=" + one());
function one {return 1;}
</script>

<script id="script2">
alert("This is script2: two=" + two());
function two() {return 2;}
</script>

下の例では、script1はコンパイル失敗し、script2は正常にコンパイル+実行されます。そしてscript3ですが、これは特に構文エラーはないのでコンパイルはOKです。しかし、存在しない関数oneを呼んでいるので実行時に例外が発生します。


<script id="script1">
alert("This is script1: one=" + one());
function one {return 1;}
</script>

<script id="script2">
alert("This is script2: two=" + two());
function two() {return 2;}
</script>

<script id="script3">
alert("This is script3:");
try {
alert("two=" + two());
alert("one=" + one());
} catch (e) {
alert("error!");
}
</script>

●対話的処理系の場合

Rhinoは、シェルのようなコマンドラインインターフェースで、1行ずつJavaScriptを実行できます。alertをprintに置き換えて、最初に挙げた例をRhinoで実行してみます。


js> print("This is script1: one=" + one());
js: "<stdin>", line 309: uncaught JavaScript runtime exception: ReferenceError: "one" is not defined.
js> function one() {return 1;}
js> print("This is script1: one=" + one());
This is script1: one=1
js>

案の定、未定義関数呼び出しは失敗します。Rhinoでは、コンパイル単位がプロンプトから入力された行になるのです。しかし、次の例を見れば、Rhinoもcompile-and-go方式であることがわかるでしょ。


js> print("This is script2: two=" + two()); function two() {return 2;}
This is script2: two=2
js>

ブラウザとRhinoが一見違った挙動を示すのは、単に、コンパイル単位が違ったから。ブラウザのscript要素をRhinoで試したいときは、全体を1行として入力するか、ファイルからロードして実行する必要があります。

●今回のまとめ

  1. JavaScript言語処理系は、コンパイラJavaScript仮想機械、オブジェクト構造からなると考えるとよい。
  2. コンパイラが一度にコンパイルする“まとまり”(ストリーム)をコンパイル単位という。
  3. ブラウザの場合、コンパイル単位はscript要素の内部、またはscript要素から参照される外部ファイルである。
  4. compile-and-go方式では、コンパイルが成功するとただちにコンパイル済みコード実行される。しかし、コンパイルが失敗すると何も起きない。
  5. ブラウザと対話的処理系が一見異なった動きをするのは、コンパイル単位が違っているからである。

実行方式に関しては、関数定義がどのようにコンパイルされるか、Functionコンストラクとeval関数、コールバックの話題などがあります。これらは次回にします。