JavaScript

変数のスコープ

 プログラムのソースコード中における変数の有効範囲を、その変数のスコープと言います。グローバル変数のスコープは、プログラム全体です。ローカル変数のスコープは、その変数が宣言された関数の中だけに限定されます。関数の仮引数もローカル変数の仲間なので、仮引数のスコープは関数の中だけに限定されます。

 関数の中にグローバル変数と同じ名前のローカル変数があった場合は、ローカル変数が優先されます。つまり、グローバル変数と同じ名前でローカル変数や仮引数を宣言すると、グローバル変数が隠されます。例を示します。

var scope = "global";
// グローバル変数を宣言する。

function checkscope() {
  var scope = "local";
  // 同じ名前でローカル変数を宣言する。
  return scope;
  // ローカル変数の値が返される。グローバル変数の値ではない。
}

checkscope()
// => "local"

 グローバル変数の場合は var 文を省略することができますが、ローカル変数は var 文で宣言しなければなりません。var 文を省略すると、次のようになります。

scope = "global";
// var 文を省略してグローバル変数を宣言する。

function checkscope2() {
  scope = "local";
  // あれ!これはグローバル変数を変更したことになるぞ。
  myscope = "local";
  // この代入で新たなグローバル変数が暗黙的に宣言される。
  return [scope, myscope];
  // 2つの値を返す。
}

checkscope2()
// => ["local", "local"]: 副作用がある。

scope
// => "local": グローバル変数が変更された。

myscope
// => "local": グローバルな名前空間に変数が追加された。

 関数は入れ子にして定義できます。入れ子にした関数は、それぞれの関数ごとに独自のローカルスコープを持ちます。各ローカルスコープは入れ子型の各層構造になります。次の例を見てください。

var scope = "global scope";
// グローバル変数。

function checkscope() {
  var scope = "local scope";
  // ローカル変数。
  function nested() {
   var scope = "nested scope";
   // 入れ子にされたローカル変数。
   return scope;
   // このスコープでの値が返される。
  }
  return nested();
}
checkscope()
// => "nested scope"

関数のスコープとホイスティング

 C 言語のようなプログラミング言語では、中括弧で囲まれたブロックごとにスコープを持つものがあります。このようなプログラミング言語では、変数は宣言されたブロックの外からは見えなくなります。このようなスコープをブロックスコープと呼びますが、JavaScript にはこのようなブロックスコープはありません。JavaScript では、その代わりに関数スコープを使っています。変数は、その変数が定義された関数と、その関数に入れ子にされている関数中からアクセスできます。次に示すコードでは、変数 i 、j 、k が異なる場所で宣言されていますが、全てスコープは同じです。3つの変数は、関数の本体のどこからでもアクセスできます。

function test(0) {
  var i = 0;
  // i は関数全体で定義。
  if (typeof o == "object") {
   var j = 0;
   // j はブロック内だけでなく、関数全体で定義。
   for(var k=0; k < 10; k++) {
   // k はループ内だけでなく、関数全体で定義。
    console.log(k);
    // 0 から 9 まで出力する。
   }
   console.log(k);
   // k は定義されたままで、10 が出力される。
  }
  console.log(j);
  // j は定義されている。ただし、初期化されない場合がある。
}

 JavaScript では関数スコープが使われるので、関数中で宣言された変数はすべて、関数全体からアクセスできます。面白いことに、変数宣言よりも前のコードからもアクセスできます。この JavaScript の特徴のことは、公式な用語ではありませんが、ホイスティング(巻き上げ)と呼ばれています。つまり、JavaScript のコードは、関数中の変数宣言を、関数の先頭に「ホイスティング」(巻き上げ)したかのように振る舞います。ただし、宣言と同時に行われる代入については巻き上げません。次のコード例を見てください。

var scope = "global";
function f() {
  var scope;
  // 関数の先頭でローカル変数は宣言される。
  cansole.log(scope);
  // ここで変数は有効。ただし値は "undefined"。
  scope = "local";
  // ここで変数を初期化し、値を設定する。
  console.log(scope);
  // そして、ここでは設定した値が保持されている。
}

 ブロックスコープを持つプログラミング言語では、変数を利用する場所のできるだけ近くで宣言するようにして、スコープをできるだけ狭くすることが、良いプログラミングスタイルだと一般的に言われています。一方で、JavaScript にはブロックスコープがないので、すべての変数を関数の先頭で宣言するようにしているプログラマもいます。このようにすることで、変数の本当のスコープとソースコードが同じになり、コードがわかりやすくなります。

プロパティとしての変数

 JavaScript のグローバル変数を宣言するというのは、実際にはグローバルオブジェクトのプロパティを定義することです。var を使って変数を宣言すると、作成されるプロパティは再定義不可(nonconfigurable。)になります。再定義不可とは、delete 演算子を使って削除できないという意味です。前述したように、非 strict モードでは、宣言していない変数に対して値を代入すると、自動的にグローバル変数を生成します。このような方法で生成された変数は、グローバルオブジェクトの通常の再定義可能(configurable)なプロパティとして生成されるので、削除することができます。以下の例を見てください。

var truevar = 1;
// 正しく宣言されたグローバル変数。削除不可。

fakevar = 2;
// グローバルオブジェクトの削除可能なプロパティを生成。

this.fakevar2 = 3;
// 先ほどの行と同じ意味。

delete truevar
// => false: 変数は削除されない。

delete fakevar
// => true: 変数は削除された。

delete this.fakevar2
// => true: 変数は削除された。

 JavaScript のグローバル変数は、グローバルオブジェクトのプロパティになります。このことは、ECMAScript 仕様で標準化されています。ローカル変数の場合には、このような要件はありません。しかし、ローカル変数についても、関数呼び出しに関連付けられたオブジェクトのプロパティとして扱うこともできます。ECMAScript 3 仕様では、このオブジェクトのことを「 Call オブジェクト」と呼んでいます。また、ECMAScript 5 仕様では「 Declarative Environment Record 」と呼んでいます。JavaScript では、this キーワードを使ってグローバルオブジェクトを参照できます。しかし、ローカル変数が保存されているオブジェクトを参照する方法はありません。ローカル変数を保持するオブジェクトについては、本来実装上の問題ですので、JavaScript プログラマが気にする必要のないことです。しかし、ローカル変数用のオブジェクトが存在するという概念は重要ですので、次の項でもう少し詳しく説明します。

スコープチェーン

 JavaScript は、構文スコープを持つ言語です。変数のスコープは、変数が定義されているソースコード行の集合と考えられます。グローバル変数はプログラム全体で有効です。ローカル変数は、宣言された関数全体と、その関数中に入れ子にされた関数群全体で有効です。

 ローカル変数を、処理系で定義されたオブジェクトのプロパティとして考えるのであれば、変数のスコープについて別の考え方もできます。JavaScript のコード(グローバルコードや関数)には、そのコードに関連付けられたスコープチェーンが存在すると考えられます。このスコープチェーンとは、そのコードに対して「スコープ内に」ある変数を定義するオブジェクトのリスト( = チェーン)のことです。JavaScript が変数 x の値を調べる必要がある場合(この処理を、変数の名前解決と呼びます)、チェーンの先頭のオブジェクトから検索を始めます。このオブジェクトが x という名前のプロパティを持つ場合、そのプロパティの値が使われます。最初のオブジェクトが x という名前のプロパティを持たない場合、チェーンの次のオブジェクトに対して検索を行います。次のオブジェクトにも x という名前のプロパティが存在しない場合には、さらに次のオブジェクト、さらにその次へと検索を進めていきます。スコープチェーンのどのオブジェクトにも x という名前のプロパティが存在しない場合、x はこのコードのスコープ内に存在せず、ReferenceError が発生します。

 トップレベルコード中(関数に含まれていないコード部分)では、スコープチェーンに含まれるオブジェクトはグローバルオブジェクト 1つだけです。入れ子になっていない関数の場合、スコープチェーンには 2つのオブジェクトが含まれます。先頭のオブジェクトには、関数の引数やローカル変数が定義され、2番目のオブジェクトがグローバルオブジェクトです。入れ子になった関数では、スコープチェーンには 3個以上のオブジェクトが含まれます。このオブジェクトのチェーンは次のように作成されます。まず、関数が定義されたときに、現在有効なスコープチェーンを保存しておきます。関数が呼び出されたときに、新たにオブジェクトを生成し、ローカル変数を保存します。そして、この新しいオブジェクトを保存しておいたスコープチェーンに追加し、関数呼び出し時のスコープチェーンを表す新たなスコープチェーンを生成します。入れ子にされた関数の場合はもっと面白いことが起こります。外側の関数が呼び出されるたびに、内側の関数が再び定義されるからです。外側の関数の呼び出しごとに、スコープチェーンが異なりますので、内側の関数を再定義するたびに差異が生じます。内側の関数のコードは同一で、変更されていないのですが、コードに関連付けられるスコープチェーンが異なるからです。

 ここで紹介したスコープチェーンという概念は、with 文を理解するときに役立ちます。また、クロージャを理解する上では必須の概念です。