IT戦記

ただただがむしゃらにソフト開発をしていたい

for 文と無名関数のイディオム

id:cho45 がチョロっと書いたコードが話題になっている

冬通りに消え行く制服ガールは✖夢物語にリアルを求めない。 - subtech
このような書き方は、自分もたまにする。
というわけで、この書き方をする利点を以下の順に解説して見る。

  1. 単純な for 文の問題点
  2. with 文を使った解決方法と、その微妙な問題点
  3. 無名関数を使った解決方法

単純な for 文の問題点

まずは、以下の HTML に対して

<ul>
 <li>hoge</li>
 <li>fuga</li>
 <li>piyo</li>
</ul>

以下の JavaScript を実行して

var list = document.querySelectorAll('ul > li');

for (var i = 0, len = list.length; i < len; i++) {
    var node = list[i];
    var text = node.textContent;
    node.onclick = function() { alert(text) }; // (1)
}

"hoge" と書いてある要素をクリックすると "piyo" と表示される。
これは、 var 文によって生成される変数は「ブロックスコープ」ではなく「関数スコープ」だからだ。
なので、 (1) で参照される変数 text は全部同じ変数、つまり、 alert の結果はすべてこのループ終了時の変数 text の値となるわけだ。

たとえば、以下の (1) と (2) は同じ変数を参照している。

function() {
  var i = 1; // (1)
  {
    var i = 0; // (2)
  }
  alert(i); // (3)
}

故に、 (3) では 0 が表示される

with 文を使った解決方法と、その微妙な問題点

これを解決するためには、 with スコープを使うことが多い。
たとえば、以下のように

for (var i = 0, len = list.length; i < len; i++) with ({ node: list[i], i: i, text: null }) {
    text = node.textContent;
    node.onclick = function() { alert(text) }; // (1)
}

with は、変数スコープをダイナミックに追加するための文で、これで作られるスコープを with スコープと読んだりもする。
この例では、 with の小括弧(丸括弧)の中に、リテラルの形でオブジェクトが指定されており、ループ中に毎回新しいオブジェクトが作られるので (1) の text は毎回違う変数となる。
これで一応は解決。
ただ、これは少し微妙な問題点を持っている。変数の参照が非常に重くなるのだ。

with スコープ内の変数参照は非常に重い

なぜ、重いか。
var 文で作られる変数の場合(eval 内を除く)は JavaScript の実行前にそのスコープで使える変数は決まってしまうので、JavaScript エンジン側での最適化がしやすい。(つまり、変数名によるスコープチェーンの探索を行わなくても大丈夫)
ただ、 with が絡んでくると「どのような変数が出現してもおかしくない状態」になるので、変数が出てくるたびに変数名によるスコープチェーンの探索を行わなければならなくなる。
つまりさっきの例で何回変数名の探索が行われるかというと

for (var i = 0, len = list.length; i < len; i++) with ({ node: list[i], i: i, text: null }) {
    text                 // 1 回
        = node.          // 2 回
            textContent;
    node.                // 3 回
        onclick =
    function() { 
        alert(           // 4 回
            text         // 5 回
        ) 
    };
}

5 回ということになってしまう。

最適化は難しい

with 文にあたえられている式がリテラルなので実行前に最適化できそうな気もするが、 JavaScript にはプロトタイプがあるので、 with 文で与えられたリテラルが with 文スコープ内で動的に変わることは十分にありえる。

with スコープでやる場合はスコープを極力小さく

with スコープを小さくしてやることは、重要。

for (var i = 0, len = list.length; i < len; i++) {
    var node = list[i];
    with ({ text: node.textContent }) {
        node.onclick = function() { alert(text) }; // (1)
    }
}

だが、 (1) の関数が大きくなってくるとその上位のスコープをいくら小さくしても、意味がない。

無名関数を使った解決方法

というわけで、これをすべて解決するのが

for (var i = 0, len = list.length; i < len; i++) (function(node, i) {
    var text = node.textContent; 
    node.onclick = function() { alert(text) };
})(list[i], i)

なのだ。
というわけで、 id:cho45 に敬礼