JavaScript では関数リテラルを変数に代入することで、その変数を関数として実行することができます。とても興味深い仕組みですが、理解するのにも時間がかかりました。特に for 構文内で関数リテラルを使って処理を定義した場合に、想定と全く違った動作をしてしまうことがありました。
具体的には下記のようなコードです。
1 2 3 4 5 6 |
var f = []; for (var x = 0; x < 3; x++){ f[x] = function () { return x; }; } console.log(f[1]()); // 3 と表示される。 |
このコードを最初に見た時、私の頭の中では 1 が表示されるとばかり思っていました。for 構文で使っている変数がカウントアップされながら、定義されるというイメージです。つまりは下記と同様の処理が行われると思い込んでいました。
1 2 3 4 5 6 |
var f = []; f[0] = function () { return 0; }; f[1] = function () { return 1; }; f[2] = function () { return 2; }; console.log(f[1]()); // 1 と表示される。 |
しかしながら、現実は違っていて for 構文を使うとループが完了したあとの 3 が表示されるのです。これはなぜでしょうか。
インターネット上の記事を調べた結果、「スコープ」がポイントであることにたどり着きました。
関数リテラルで関数を定義すると、記述した場所のスコープで定義されます。function () { return x; }
が記述されたときには、この関数内に x はありませんので、一つ外側のスコープを探しに行きます。for構文内はスコープではありませんので、結果的に「関数が実行されたらグローバルスコープの x を参照しにいく」という動作がf[0]、f[1]、f[2]に定義されます。(動作は全部同じ!)これらの関数の実行時のグローバルスコープにある x はfor構文でカウンターとして使い終わった変数ですから、f[0]、f[1]、f[2]どれを実行しても 3 が返されることになります。
では、次の疑問として function () { return x; }
のなかの x を、for 構文で使っている x を参照するようにしたい場合はどうしたらいいのでしょうか。
即時関数を使うとスコープを作ることができます。その即時間数に引数として、カウンターで使われている x を渡すことで、f[x] = function () { return x; };
で使われている一つ外側のスコープに、カウンターで使われている x が存在することになります。したがって、fxの実行時には x が 0、1、2 のスコープをそれぞれ参照しにいきますので、期待通りの結果を得ることができます。
1 2 3 4 5 6 7 8 |
var f = []; for (var x = 0; x < 3; x++){ (function(x){ f[x] = function () { return x; }; })(x); } console.log(f[1]()); // 1 と表示される。 |
with構文を使うことで即時関数を使った場合と同じようなことができます。with({x:x}){ … } とすることで、withの外側の x をwithの内側の x として使うことができるスコープを作り出すことができるようになります。外側の x はfor構文によってカウントアップされていますから、with({x:0}){ … }、with({x:1}){ … }、with({x:2}){ … }の3回スコープが作成されることになります。実行時には f[x] = function () { return x; };
の定義時に一つ外側で使われているスコープの x を参照しにいくことになるため、即時関数のときと同様の結果を得ることができます。
1 2 3 4 5 6 7 8 |
var f = []; for (var x = 0; x < 3; x++){ with({x:x}){ f[x] = function () { return x; }; } } console.log(f[1]()); // 1 と表示される。 |