おれの技術日記

元はJava+SQLがメインのエンジニア、フロントエンドは軽くかじった程度で苦手。最近忘れっぽいので覚えたことをいろいろメモするためにブログ開始。

JavaScript再入門2 - スコープチェーンとクロージャ

JavaScript再入門1- プロトタイプチェーンとコンストラクタ関数 - おれの技術日記の続き。

例によってこちらにお世話になります。
最強オブジェクト指向言語 JavaScript 再入門!

4. 変数のスコープはグローバル/ローカルのみ、スコープチェーンのルールに従い内側のスコープから順次走査・参照される

CとかJavaとかだとローカル変数のスコープはブロックを単位としているので、例えば以下のようなコードはコンパイルエラーになってそもそも実行できないが、JavaScriptでは合法。ここで宣言された変数a, bはその関数内のどこでも参照することができる。

function func1(){
    var a = 1;
    if(何らかの条件){
        var b = 3;
    }
    alert(b);
}

JavaScriptはみょうちくりんな言語なので関数を入れ子にすることもできるんだが、では次にこうするとどうなるか。

function func1(){
    var a = 1;
    function func2(){
        var b = 2;
    }
    alert(b);//ここでReferenceError
}
func1();

内側の関数の中で宣言された変数に対して外側の関数からはアクセスすることができず、ReferenceErrorとなる。ブロックレベルのスコープは存在しないが関数の入れ子による変数スコープの制限は有効。

グローバル変数 ローカル変数
関数の外で宣言した変数 関数の内側で宣言した変数・関数の仮引数
プロセス全体で参照できる その関数の内側からのみ参照できる

次にこんなコードを書いてみると、これは動く。

var c = 3
function func1(){
    var a = 1;
    function func2(){
        var b = 2;
        //一番内側の関数からはその外側の変数を参照できる
        alert(a+b+c);//"6"となる
    }
    func2();
}
func1();

func2で参照された変数は一番内側のスコープをまず走査し、見つからなければもうひとつ外のfunc1のスコープを走査し、最後にグローバルスコープを走査する(もしそれでも見つかれなければエラーになる)。このようにして内側から外側に順次走査する機構をスコープチェーンという。ちなみに、複数のスコープで同一名の変数を宣言した場合には、このルールに従って一番最初に見つかったほう(=より内側の変数)が参照される。

5. クロージャを使ってグローバルスコープを汚さずに状態を管理する

先ほど行ったようにJavaScriptでは関数の中にさらに関数を定義することができるが、内側の関数をスコープ外から呼び出すことができるか。と思ってこんなコードを書いてみたら、やはり最後の行のfunc2()は参照できずエラーになった。

function func1(){
    var a = 1;
    function func2(){
        var b = 2;
        alert(a+b);
    }
    func2();
}
func1();//これはOK
func2();//これはエラーになる

このスコープの妙を利用して、以下のように関数内に変数の状態を保持させ続けることができる。このようなやり方をクロージャという。

function func1(){
    var a = 1;
    return function(){
        alert(a);
        a+=1;
    }
}
var f = func1();
var f2 = func1();
f();//1がアラートされる
f();//2がアラートされる
f();//3がアラートされる
f2();//1がアラートされる

ちなみに、「この程度ならグローバル変数でもできるのでは?」と思われる方もいると思うが(というか私も思った)、クロージャはグローバルスコープを汚さずに状態を維持するために使うらしい。クロージャの手順は以下の通り。

  1. 関数を入れ子に定義する
  2. 外側の関数に変数を宣言する
  3. 内側の関数からその変数を操作する
  4. 内側の関数を外側の関数の戻り値にする

依然として疑問なのは

  • この例だと関数の参照をグローバルで持ち続けないといけないから結局グローバルスコープは汚れるのでは?
  • JavaでいうとこのBeanオブジェクトみたいなん作るときにうれしいのかな?でもそれならコンストラクタ関数でもおんなじことできるしな・・・
  • 結局使いどころがピンとこないぞ、ググってもカウンタの例ばっかり出てくる

まあ、これはおいおいわかってくるんでしょう・・

依然として今後の課題

  1. Apply関数って何?
  2. (function() {})();とかいう記法→即時関数というらしい
  3. thisのスコープの考え方