JavaScriptで本格プログラミング(3)

前回から結構あいだが開きましたが、今回はvar宣言について述べてみます。
var宣言と関数の関わりについて以下に例を出してみます。

やりたいこと

例えば以下のようなオブジェクトtargetがあるとき、


var target = {

  onclick : function() {

    alert("Click target!");

  },

  onfocus : function() {

    alert("Focus target!");

  }

};

このオブジェクトが直接持つon〜というメソッドを次のように上書きします。

target.on〜 = function() {

  alert("on〜");

};

例えば"onclick"を決め打ちであれば

target.onclick = function() {

  alert("onclick");

};

でよいですが、on〜が複数あり、何があるかわからない状況で、この変換を行う関数を作成することが目的です。

試行1

最初に考えるのは、for〜inループでon〜関数を付け替える方法だと思います。


function test(obj) {

  for(var i in obj) {

    if(i.indexOf("on", 0) == 0) {

      obj[i] = function() {

        alert(i);

      };

    }

  }

}

このコードを上記のtargetに対して実行すると以下のようにうまくいきません。

test(target);

target.onclick();  // onfocus

target.onfocus();  // onfocus

この理由は、test関数内で宣言した変数ionclickを付け替えたときとonfocusを付け替えたときで共有されていることにあります。
つまり、ループ1周目では変数iは確かにonclickでしたが、2周目にはonfocusになってしまいます。
そして1周目でも2周目でも変数の入れ物は同じですから、target.onclick();を実行してもtarget.onfocus();を実行しても、同じ変数iの内容を表示します。


試行2

では、ループの中で宣言している変数に代入しなおしてみてはどうでしょうか?


function test2(obj) {

  for(var i in obj) {

    if(i.indexOf("on", 0) == 0) {

      var name = i;

      obj[name] = function() {

        alert(name);

      };

    }

  }

}

これも残念ながら結果は一緒です。

test2(target);

target.onclick();  // onfocus

target.onfocus();  // onfocus

変数のスコープは宣言された関数内*1となりますが、関数が実行される度に変数が用意されます。つまり、関数の1回実行につき1つの入れ物となります。
test2関数の中でvar宣言のコードは何度も通過しますが、用意された変数nameは1つだけで、ループのたびに同じ入れ物に変数iの中身を代入します。
target.onclick();target.onfocus();も、同じ変数nameを参照しています。

試行3

これより、ループの1周ごとに異なる変数を用意するのも1つの解決案です。
これは、ループの内側に匿名関数を書くことで実現できます。


function test3(obj) {

  for(var i in obj) {

    if(i.indexOf("on", 0) == 0) {

      (function() {

        var name = i;

        obj[name] = function() {

          alert(name);

        };

      })();

    }

  }

}

これで期待した結果を得ることができました。

test3(target);

target.onclick();  // onclick

target.onfocus();  // onfocus

この匿名関数はfor〜inループの中でonclickonfocusでの2度実行されます。さらにその中の変数nameは、匿名関数が実行される度ごとに異なる入れ物となります。
つまり、ループがonclickのときの変数nameと、onfocusのときの変数nameは互いに独立しています。
そして、匿名関数の中でtarget.onclicktarget.onfocusを上書きしていますが、いざtarget.onclickが実行されるときはそれが実装されたときのnameを、target.onfocusも同様に実行時は実装されたときのnameをそれぞれ参照します。


※匿名関数でのnameの定義やiの受け渡しは引数を使うこともできます。


function test3(obj) {

  for(var i in obj) {

    if(i.indexOf("on", 0) == 0) {

      (function(obj, name) {

        obj[name] = function() {

          alert(name);

        };

      })(obj, i);

    }

  }

}

ポイント

注目したいのは、

  1. var宣言された変数は、そのすぐ外側の関数が実行される度ごとに生成されていること
  2. var宣言された変数は、そのすぐ外側の関数が実行し終わっても、(スコープ内)からであれば参照できること

の2点です。
これを利用することで、privateなフィールドとそれに対するpublicなアクセスメソッドを実装することができます。

*1:他の多くの言語のように、変数宣言のすぐ外側の中括弧ブロック内ではありません。JavaScriptではすぐ外側の関数ブロックとなります。