JavaScript

評価式

 多くのインタプリタ言語と同じように、JavaScript には、JavaScript のソースコード文字列を解釈する機能があります。そして、この文字列を評価することで値を生成します。JavaScript では、グローバル関数の eval() を使って評価します。例を見てください。

eval("3+2")
// => 5

 ソースコード文字列を動的に評価する機能は、プログラミング言語にとって強力な機能ですが、実際には、必要になる場面はほとんどありません。eval() を使いたくなった場合も、本当に使う必用があるのかどうかをよく考えてみてください。

 以降では、eval() の基本的な使い方について説明します。その後、最適化処理にあまり影響がないように機能制限された eval() について紹介します。

eval()

 eval() の引数は 1つです。引数として文字列以外の値を渡した場合には、その値がそのまま返されます。文字列が渡された場合、文字列を JavaScript コードとして解釈します。解釈できない場合は、SyntaxError 例外をスローします。文字列が解釈できた場合には、コードを評価し、文字列中の最後の式または文の値を返します。最後の式または文が値を持たない場合には、undefined が返されます。文字列中から例外をスローする場合は、eval() はその例外をスローし直します。

 eval() を呼び出すときに重要な点は、eval() を呼び出したコードの環境(変数)が使われるという点です。つまり、ローカルコードと同じように、変数の値を検索し、同じように新しい変数や関数を定義する、ということです。ある関数の中でローカル変数 x を定義しておき、eval("x") という風に呼び出すと、ローカル変数の値が得られます。eval("x=1") という風に呼び出せば、ローカル変数の値が変更されます。また、eval("var y = 3;") という風に呼び出せば、新たにローカル変数 y が宣言されます。同様に、以下のようなコードでローカル関数も宣言できます。

eval("function f() { return x+1; }");

 eval() をトップレベルコードから呼び出した場合は、もちろん、グローバル変数やグローバル関数に対して処理を行うことになります。

 eval() に渡すコード文字列は、構文上はその文字列に閉じていることには注意してください。つまり、コード文字列を使って、関数の内容を書き換えることはできません。例えば、eval("return;") と記述しても意味がありません。return は関数中でしか意味がありません。評価する文字列が呼び出し元の関数と同じ変数環境を使うと説明しましたが、これは文字列自体が関数の一部になるという意味ではありません。なお、文字列が単独のスクリプトとして意味があれば、( x=0 のような短いスクリプトでも)、eval() の引数として問題ありません。それ以外の場合は、SyntaxError がスローされます。

グローバル eval()

 eval() が JavaScript の最適化処理において問題となるのは、ローカル変数を変更できる機能です。しかし、この問題への対策として、インタプリタは eval() を呼び出す関数については最適化処理をあまり行わないようにしています。しかし、スクリプト中で eval() の別名を定義し、この別名を使って関数を呼び出している場合、JavaScript インタプリタはどのような処理をするのでしょうか。JavaScript インタプリタの開発者の負担を減らすために、ECMAScript 3 仕様では、別名を定義できるようにする必要はない、と宣言しています。つまり、eval() 関数が「 eval 」以外の名前で呼び出された場合は、EvalError 例外をスローしても良いことになっています。

 実際には、インタプリタ開発者は他の対策を施している場合が多いようです。eval() を別名で呼び出した場合に、eval() 中では、文字列をトップレベルのグローバルコードとして評価するようにしています。評価されたコード中から、グローバル変数やグローバル関数を定義したり、グローバル変数を設定したりできますが、呼び出した関数のローカル変数を使ったり変更したりはできないようにしています。これにより、呼び出した関数に対する最適化処理には影響が出ないようになります。

 ECMAScript 5 では、EvalError 例外を過去のものにして、eval() のデファクトの振る舞いを標準化しています。eval() 関数を「 eval 」という名前で呼び出した場合を「直接 eval 」と呼びます(「 eval 」が予約語のように使われ始めています)。直接 eval() を呼び出した場合は、呼び出しコンテキストの変数環境を使います。このほかの場合(「間接呼び出し」)は、変数環境としてグローバルオブジェクトを使います。ローカル変数や関数の読み書き、定義はできません。以下の例を見てください。

var geval = eval;
// 別名を使うとグローバル eval になる。

var x = "global", y = "global";
// 2つのグローバル変数。

function f() {
// この関数はローカル eval を使う。
  var x = "local";
  // ローカル変数を定義する。

  eval("x += 'changed';");
  // 直接 eval ではローカル変数を設定する。

  return x;
  // 変更されたローカル変数が返される。
}

function g() {
// この関数はグローバル eval を使う。
  var y = "local";
  // ローカル変数。

  geval("y += 'changed';");
  // 間接 eval では、グローバル変数を設定する。

  return y;
  // 変更されていないローカル変数が返される。
}

console.log(f(), x);
// ローカル変数が変更される。"localchanged global" が出力される。

console.log(g(), y);
// グローバル変数が変更される。"local globalchanged" が出力される。

 グローバル eval の機能は、最適化のための妥協の産物にすぎないわけではありません。この機能は非常に便利です。グローバル eval を使えば、コード文字列を独立したトップレベルのスクリプトとして実行できます。このページの最初でも述べましたが、コード文字列を評価しなければならない場合はあまりありません。しかし、必要となる場合があるとしたら、ローカル eval ではなく、グローバル eval を使う場合でしょう。

 IE9 よりも前のバージョンでは、IE の挙動はほかのブラウザと異なる挙動をします。eval() が他の名前で呼び出されたとしても、グローバル eval を行いません( EvalError 例外もスローしません。単にローカル eval を行うだけです)。しかし、IE では、execScript() という名前のグローバル関数が定義されています。この execScript() を使えば、トップレベルスクリプトとして、引数で指定した文字列を実行できます(しかし、eval() とは異なり、execScript() は常に null を返します)。

strict モードでの eval()

 ECMAScript 5 の strict モードでは、eval() 関数の振る舞いにさらに制限が課せられています。さらに、「 eval 」という識別子の利用についても制限が追加されています。strict モードのコードから eval() が呼び出された時、または、"use strict" ディレクティブから始まるコード文字列が評価された時、eval() はプライベートな変数環境を使ってローカル eval を行います。つまり、strict モードでは、評価されたコードからはローカル変数を取得したり、変更したりできますが、ローカルスコープ中に新しい変数や関数を定義できません。

 さらに、strict モードでは、eval() をもっと演算子のような扱いにしています。このため、「 eval 」を予約語にすることで、eval() 関数を新しい関数で上書きできないようにしています。「 eval 」という名前で、変数や関数、関数引数、キャッチブロックの引数を宣言することもできません。