30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

スコープとクロージャ

2016.8.27追記
ES2015で登場したletconstブロックスコープを持つので、そちらも参照されたい。
var,let,constの違いは、ブロックスコープと巻き上げ

グローバルスコープとローカルスコープ

スコープとは、変数を参照できる範囲のこと。
スコープ外の変数には、アクセス出来ない。

JavaScriptにおけるスコープは、以下の3つ。

  • グローバルスコープ
  • ローカルスコープ
  • evalスコープ

このうちevalスコープについては、実務で使うことはまずないと思われるので、ここでは触れない。

JavaScriptは必ず、グローバルスコープを1つ持つ。
全てのコードは、グローバルスコープ(グローバルオブジェクト)の中に書かれている、と言える。
グローバルスコープにある変数(グローバル変数)には、どこからでもアクセスできる。

ローカルスコープは、関数ごとに作られる。
ローカルスコープにある変数(ローカル変数)には、その関数の中でのみ、アクセスできる。

// グローバルスコープ

var a = 'This is global.';

(function(){
    // ローカルスコープ
    var x = 'x';
    console.log(a); // This is global.
    console.log(x); // x
})();

(function(){
    // ローカルスコープ
    var y = 'y';
    console.log(a); // This is global.
    console.log(y); // y
})();

console.log(a)  // This is global.
console.log(x); // ReferenceError: x is not defined

ifやforはスコープを持たない

JavaScriptのスコープは、上述の3種類だけである。
iffor{}はスコープを持たない。

if(true){
    var i = 0;
};

console.log(i); // 0
i = 99;
console.log(i); // 99

for(var x=0; x < 1; x++){
    i++;
    console.log(i); // 100
};
console.log(x); // 1

// ifやforの{}は、スコープを持たない

スコープチェーン

ローカルスコープで該当する変数が見つからなかった場合、グローバルスコープから変数を探す。

var x = 'x of global.';
var y = 'y of global.';

(function(){
    var x = 'x of local.';
    console.log(x); // x of local.
    console.log(y); // y of global.
})();

関数が入れ子になっている場合は、処理が発生したスコープから順番に辿っていく。

var x = 'x of global.';
var y = 'y of global.';
var z = 'z of global.';

var outerScope = function(){
    var x = 'x of outer';
    var y = 'y of outer';
    var innerScope = function(){
        var x = 'x of inner';
        console.log(x);
        console.log(y);
        console.log(z);
        console.log(a);
    }();
};

outerScope();
// x of inner
// y of outer
// z of global.
// ReferenceError: a is not defined

上記の例ではまず、innerScopeのローカルスコープを探す。次にouterScopeのローカルスコープを探す。次にグローバルスコープ。

この一連のプロセスは、グローバルスコープに行き着くまでどこまでも続く。
そしてグローバルスコープでも見つからなければ、エラーとなる。

この仕組みのことを、スコープチェーンと呼ぶ。

ローカル変数の有効範囲

スコープチェーンはシンプルな仕組みだが、var宣言のタイミングには注意が必要。

var scope = 'This is global.';

var myFunc = function(){
    console.log(scope);
    var scope = 'This is local.';
    console.log(scope);
};

myFunc();
// undefined
// This is local.

上記の例では、最初のconsole.log(scope);でなぜかundefinedを返している。

次の行でscopeを定義しているので、このローカルスコープでは、scopeが有効になっている。
だから、スコープチェーンを辿ってThis is global.を返すことはない。

しかし実際に宣言して値を定義しているのは、ローカルスコープの2行目である。
そのため、最初のconsole.log(scope);の時点では、ローカル変数scopeが「存在するが未定義」という状態になってしまい、undefinedを返したのである。

スコープは定義時に決まる

JavaScriptの関数は高階関数なので、引数として渡したり、戻り値として受け取ったりすることが出来る。
しかしスコープは、それらに影響されることはない。

関数がどのように呼び出されるかは、スコープに影響を与えない。
スコープはあくまでも、定義時に決まり、それが変わることはない。

この仕組みを利用したのが、クロージャである。

クロージャ

クロージャとは、ローカルスコープを保持した、関数内関数である。

下記の例では、無名関数がクロージャである。

function originFunc(){
    var i = 'local';
    return function(){
        console.log(i);
    };
};

var closure = originFunc();

closure();  // local

無名関数は、スコープチェーンを辿って、変数iにアクセスできる。
その無名関数が closureに格納されている。closureを実行した場所はグローバルスコープであり、originFuncのスコープにはアクセスできないはずだが、closureに格納されている無名関数がローカルスコープを保持しているため、iにアクセスできる。

繰り返しになるが、スコープは関数の定義時に決まる。その関数がどのような文脈で呼び出されるかは無関係である。
そのため下記のoriginFunc()globalを返す。

var x = 'global';

function originFunc(){
    console.log(x);
};

(function(){
    var x = 'local';
    originFunc();
})();
// global