■関数リテラルとFunctionコンストラクタにおけるスコープの違い
前回、関数を定義する構文として「関数リテラル」と「Functionコンストラクタ」について紹介した。
関数リテラル、Functionコンストラクタは構文的な違いこそあるものの、いずれも匿名関数を定義するという意味でほぼ同様の機能を提供する。ただし、関数内で入れ子に関数リテラル/Functionコンストラクタを使用した場合には、スコープの解釈が互いに異なる点に要注意。
以下に、具体的なコード例を挙げてみよう。
var num = 0;
function scope() {
var num = 1;
// Functionコンストラクタ
var myScope1 = new Function("", "window.alert(num);");
// 関数リテラル
var myScope2 = function() { window.alert(num); };
myScope1(); // 0
myScope2(); // 1
}
scope();
結果を見ても分かるように、Functionコンストラクタはグローバル変数numを参照しているのに対して、関数リテラルがローカル変数numを参照しているのだ。
直感的には混乱しやすい挙動ではあるが、「Standard ECMA-262 3rd Edition - December 1999(PDF)」の「15.3.2.1 new Function (p1, p2, … , pn, body)」項16を確認しても、「Pass in a scope chain consisting of the global object as the Scope parameter.(スコープとしてグローバル・オブジェクトで構成されるスコープ・チェーンを引き渡す)」とあり、この動作が正しいことが理解できる(スコープ・チェーンについては、後述のコラムを参照)。
■仮引数のスコープと参照型
仮引数とは、関数に引き渡される変数のことだ。仮引数に指定した変数は、通常、ローカル変数として処理される。以下のコードで、実際にそのことを確認してみよう。
var x = 1; [A]
function ref(x) {
x++;
return x;
}
window.alert(ref(10)); // 11 [B]
window.alert(x); // 1 [C]
ここでは、まず[A]でグローバル変数xに「1」が代入される。次に[B]によって関数refが呼び出されるわけであるが、その内部で使用されている仮引数xはローカル変数の扱いであるので、これをいくら操作してもグローバル変数xには影響を及ぼさない。実際、ここではローカル変数xにまず「10」を与え、その後、1インクリメントしているわけであるが、[C]の結果を見ても、もともとのグローバル変数xには影響が及んでいないことが確認できる。
ここまでは比較的容易に理解できるところであるが、これに先ほども紹介した参照型が絡んでくると、(難しくはないが)やや混乱するおそれがあるので注意されたい。例えば、リスト16のようなコードを見てみよう。
var x = [0, 1, 2]; // 配列 [A]
function ref(x) {
x[0] = 10;
return x;
}
window.alert(ref(x)); // [10,1,2] [B]
window.alert(x); // [10,1,2] [C]
前述したように、JavaScriptにおいて配列は参照型であり、参照型の値は参照渡しされる。つまり、この場合、まず[A]でグローバル変数xに配列が代入される。次に[B]で関数refが呼び出され、仮引数xが生成される。繰り返しであるが、仮引数は自動的にローカル変数と見なされるので、ここで[A]のグローバル変数xと仮引数(ローカル変数)xは別物である。しかし、値自体は参照渡しされているので、結果的に同一の値を参照しているわけだ。従って、関数の中で配列に対して操作を行った場合、その結果はグローバル変数にも反映されることになる([C])。
このような挙動は、参照型の概念を理解してさえいれば、難なく理解できるものであるが、グローバル変数/ローカル変数とも絡んでくると混乱のモトとなるので、確と整理しておきたい。
[コラム]グローバル変数とローカル変数の実体
前回も触れたように、JavaScriptは関数呼び出しのタイミングで、ローカル変数(仮引数を含む)を、Activation Object(通称「Callオブジェクト」)のプロパティとして格納している。
とすると、グローバル変数も何らかのオブジェクトのプロパティなのではないかと思えてきた方はご明察。JavaScriptは、JavaScriptコードを実行するタイミングで、グローバル変数を「Globalオブジェクト」のプロパティとして格納している*2。
*2 同様に、JavaScriptで提供されているevalやisNaN、isFiniteのような関数も、その実体はGlobalオブジェクトのメソッドであると解釈できる。
もっとも、Globalオブジェクトは(先のCallオブジェクトがそうであったように)アプリケーション側から明示的に生成したり呼び出したりすることはできないし、通常は意識すらすることのない存在であるのだが、Global/Callオブジェクトを意識してみると見えてくることもある。
繰り返しであるが、JavaScriptはコードの実行時、関数の呼び出し時に、それぞれ新しいGlobal/Callオブジェクトを生成する。これらのオブジェクトを、呼び出しの順に連結したリストを「スコープ・チェーン」と呼ぶ。
JavaScriptでは、変数を解決する際に、このスコープ・チェーンの先頭に位置するオブジェクトから順にプロパティを検索して、合致したプロパティが見つかればその値を、見つからなければ次のオブジェクトを検索するのだ。
例えば、入れ子の関数内で記述された変数の場合、スコープ・チェーンには、先頭から、内部のCallオブジェクト、外部のCallオブジェクト、そして、Globalオブジェクトが含まれているはずだ。そこで、この順番でプロパティの有無が確認され、最初に見つかったところの値が返されるというわけだ。最末尾のGlobalオブジェクトでも合致するプロパティが見つからなかった場合、その変数は未定義であると見なされる。
なお、スコープ・チェーンは、それぞれの実行コンテキスト(個々の関数の実行)ごとに形成される。これによって、JavaScriptは実行コンテキストごとにローカル変数の独立性を保証しているわけである。
Copyright© Digital Advantage Corp. All Rights Reserved.