変数のスコープ | JavaScript | プログラミング

JavaScriptはブロックスコープやモジュールスコープを持たない。変数は大域変数か関数内局所変数のどちらかだ。これは不便なことだ。が、関数スコープを、ブロックスコープやモジュールスコープの代用とすることが広く行われていて、まー何とかなっているのである。今日はこの事情を説明しよう。

JavaScriptではブロックスコープの変数は作れない

次の簡単なサンプルを見てみよう。

function test() {
    var x = 1;
    console.log(x);   // これは1が表示される
    if (true) {
       var x = 2;    
       console.log(x);// これは2が表示される
    }
    console.log(x);   // 何が表示される?
}

コンソールに三番目に表示される値は何だろう? JavaScriptをよく知らないが他の言語の経験がある人なら、「1」が出力されると考えるだろう。なぜなら、var x = 2; の部分は、ifブロックの内側にあり、同じ名前の変数でも別物のはずだからだ。

ところが、この「はず」が通用しない。JavaScriptでは、変数がどこで宣言されようが同じ名前の変数は1個しか作れない。関数のトップレベルで var x = 1; と宣言(と初期化)された変数も、ifブロックの内側で var x = 2; と宣言された変数もJavaScriptでは同じ変数なのだ。(だから、三番目に「2」が表示される。)

JavaScriptに慣れている人は逆に、この挙動に不思議さも違和感も感じないかもしれないが、他言語からの類推で誤解していると痛い目にあう。ブロックの内側における変数への代入は外側にも影響を及ぼす。

JavaScriptではモジュールスコープの変数は作れない

JavaScriptには、正式なモジュールという概念はない。ここでは、1つのソースファイルをモジュールと考えることにする。多くのプログラミング言語では、モジュール内でだけ可視で、モジュールの外部からは見えない変数を定義できる。

JavaScriptでは、そのようなモジュール(ファイル)固有の変数を作れない。モジュール foo.js で宣言された変数xと、モジュール bar.js で宣言された変数xは、同じ大域変数とみなされてしまう。

// foo.js

var x = 1;

// ...

// bar.js

var x = 2;

// ...

JavaScriptのインライン関数

JavaScriptのとても便利で特徴的な機能として、関数をリテラル(直接表現)として書けることがある。これは、関数を値のように扱えることだ。例えば、次のような書き方はしばしば見るだろう。


var hello = function(){console.log('hello');};

このコードは、helloという変数に右辺の関数を値として代入していることになる。結果的に、関数にhelloという名前が付き、以下の関数定義と等価になる。

function hello { 
    console.log('hello');
};

function(){console.log(‘hello’);} のような形で表現された関数は無名関数とか匿名関数と呼ばれるが、実は名前を持つことができる。そこでここでは、リテラル表現された関数をインライン関数と呼ぶことにする。名前付きのインライン関数は、function hello(){console.log(‘hello’);} のように書ける。次のようにすると、公式の呼び名と内部的な名前が違った関数を作れる。


var greeting = function hello(){console.log('hello');};

greeting関数はもちろんgreetingという名前(公式の名前)で呼ばれるが、greeting.name の値は “hello” となる。余談だが、インライン関数に付けた名前は再帰呼び出しのために使える。

インライン関数を使って変数のスコープを作る

JavaScriptの変数は大域変数か関数内局所変数のどちらかである。つまり、変数の値をある範囲内に閉じ込めて外部に漏れないようにするには関数内局所変数を使うしかない。単なるブロック { … } の代わりに、(function(){ … }) というインライン関数と即時実行(最後の丸括弧)を使う。

先に挙げたブロックスコープの例とモジュールスコープの例は次のようになる。

function test() {
    var x = 1;
    console.log(x);
    if (true) {
       (function() {
         var x = 2;    
         console.log(x);
       })();
    }
    console.log(x);
}
// foo.js

(function() {
var x = 1;

// ...

})();

// bar.js

(function() {
var x = 2;

// ...

})();

相当に不格好になってしまうが、目的は達せられる。

オマジナイの意味を理解する

閉じたスコープを作るには、(function() { と })() で囲めばよい。これがルールだ。このルールの根拠は何だろうか?

まず、インライン関数は関数を直接表現する式だ。あるいは、関数を値とする式だとも言える。だから次のようにして関数を定義できるのだった。

var myfun = 
       (function() {
         var x = 2;    
         console.log(x);
       });

この代入文の右辺は関数である。よって、変数myfunの値は関数そのものとなる。単にmyfunとしても関数を実行できない。myfun() と丸括弧を付けるのだった。この事情はインライン関数でも同じで、(インライン関数)() と最後に丸括弧を付ければ(必要なら引数を指定して)関数が実行される。

つまり、(function() { … })() という書き方は、… の部分が定義されてすぐに実行されることになる。そして、実行中に宣言して使った変数は関数内局所変数だから外部に影響を及ぼさない。

さらに、(function() { … })() の … の部分からは、外側のスコープの変数も見えているために、入れ子のスコープの機能を持つ。構文は不格好で読み書きが面倒だが、このオマジナイっぽいルールによりJavaScriptに欠けていた機能 — つまりブロックスコープやモジュールスコープが実現できることになるのだ。