あと味

たくさん情報を食べて、たくさん発信すると、あとになって味わい深い。

JavaScriptのfor文の中で、カウンタ変数を利用する関数をジェネレートするいくつかの方法

for文の中で、カウンタ変数を利用する関数を作るとき、はじめは必ずハマるであろうことが予想できます。

私も実際にハマったことが多々あります。

本エントリーでは、for文の中で、カウンタ変数を利用する関数をジェネレートするいくつかの方法を提示したいと思います。

問題のあるコード

以下のコードがfor文の中で、カウンタ変数を利用する関数をジェネレートするコードです。for文を1から5まで繰り返し、配列の中にカウンタ変数を出力する関数を格納していきます。関数が格納された配列をさらにfor文で走査し、1から5まで出力することを意図しています。

素直にコーディングすると、まずうまくいきません。

var func_list = [];
for (var i = 1; i < 6; i++) {
  func_list.push(function() { return console.log(i); }); 
}

for (var j = 0; j < func_list.length; j++) {
  func_list[j]();
};

// 結果:
// 66666

関数をジェネレートする関数内にカウンタ変数がありますが、評価する際にはiのカウンタが回りきって、すべて6になってしまいます。

これを、以下のそれぞれの方法で意図する動きにします。

カウンタ変数をコピーしておく

最も簡単な解決方法が、カウンタ変数をコピーしておくという方法です。

var func_list = [];
var counter_list = [];
for (var i = 1; i < 6; i++) {
  counter_list.push(i);
  func_list.push(function() { return console.log(counter_list.shift()); }); 
}
for (var j = 0; j < func_list.length; j++) {
  func_list[j]();
};

couter_listという配列にカウンタ変数をあらかじめコピーしておきます。そうすれば問題なく意図するコードとなります。でも、これは根本的な解決になってないし、タイトルの定義ともずれるので、微妙です。

evalを使う

evalを使うとうまくいきます。

var func_list = [];
for (var i = 1; i < 6; i++) {
  func_list.push(eval('(function() { return console.log(' + i + '); })'));
}

for (var j = 0; j < func_list.length; j++) {
  func_list[j]();
};

evalを使うことでカウンタ変数の束縛に成功します。id:perlcodesampleさんの記事を見て初めて知ったんですけど、evalって関数オブジェクトを直接引数にいれることはできないんですね。関数のオブジェクトを()(丸括弧)で囲んで、式化しておかないといけないようです。

参考: サンプルコードによるPerl入門

new Function()を使う

evalと似てます。

var func_list = [];
for (var i = 1; i < 6; i++) {
  func_list.push(new Function('return console.log(' + i + ');'));
}

for (var j = 0; j < func_list.length; j++) {
  func_list[j]();
};

カウンタ変数を束縛することに成功します。

基本的に、evalされる文字列に変数を含める場合は、束縛ができるっぽいですね。

無名関数を使う(クロージャを使う)

最も一般的な方法が無名関数を使う方法のようです。他の方法に比べてパフォーマンスが良いみたいです。私も最近はこれで解決しています。

var func_list = [];
for (var i = 1; i < 6; i++) {
  (function(i) {
    func_list.push(function() { return console.log(i); });
  })(i); 
}

for (var j = 0; j < func_list.length; j++) {
  func_list[j]();
};

無名関数の位置を変えた以下のパターンでもいけます。

var func_list = [];
for (var i = 1; i < 6; i++) {
  func_list.push(
    (function(i) { return function() { return console.log(i); } })(i)
  ); 
}

for (var j = 0; j < func_list.length; j++) {
  func_list[j]();
};

楽だから無名関数を使いますが、本質的にはクロージャを使うということです。以下のコードでもカウンタ変数を束縛できます。

var func_list = [];
var func = function(i) {
  return func_list.push(function() { return console.log(i); });
}
for (var i = 1; i < 6; i++) {
  func(i);
}

for (var j = 0; j < func_list.length; j++) {
  func_list[j]();
};

最終的に配列に格納する関数がクロージャになっているので、外側で定義された環境(カウンタ変数)を使うことができます。for文の中に定義してもパフォーマンスの無駄なので、外に出しています。クロージャってほんとに不思議。

letを使う

JavaScript1.7以降限定ですが、letでもOKです。サポートするブラウザが増えれば、簡単だしパフォーマンスもいいみたいなので、今後、これが主流になると思われます。(問題がある書き方だったので最後の追記参照)

まずはlet文を使う方法。

var func_list = [];
for (var i = 1; i < 6; i++) {
  let (i = i) {
    func_list.push(function() { return console.log(i); }); 
  }
}

for (var j = 0; j < func_list.length; j++) {
  func_list[j]();
};

簡潔にかけて素敵。

次はlet式。

var func_list = [];
for (var i = 1; i < 6; i++) {
  func_list.push(let (i = i) function() { return console.log(i); }); 
}

for (var j = 0; j < func_list.length; j++) {
  func_list[j]();
};

で、最後はlet定義。

var func_list = [];
for (var i = 1; i < 6; i++) {
  {
    let j = i;
    func_list.push(function() { return console.log(j); }); 
  }
}

for (var j = 0; j < func_list.length; j++) {
  func_list[j]();
};

個人的にはlet文が見やすく好きかな。let文とlet定義は、スコープが変わることを明示するために、省略可能な{}(ブレース)も書いといた方がいいと思います。早く対応して欲しい。

補足: let定義のハマりどころ

let定義を使った時にハマった箇所について紹介します。

上のコードではなく、最初は以下のコードを書いていました。

var func_list = [];
for (var i = 1; i < 6; i++) {
  let i = i;
  func_list.push(function() { return console.log(i); }); 
}

for (var j = 0; j < func_list.length; j++) {
  func_list[j]();
};

// 結果:
// undefined(5つ分)

let文やlet式のように、iにiを束縛しようとしたら、なぜかundefinedになってしまいました。何でだろうと思ってMDCをよく読むと同じようなことが書いてあったので理解できました。

参考: New in JavaScript 1.7 - MDC

let定義の右辺は、左辺で束縛する変数名と同じスコープに属するらしく、この場合、右辺のiは左辺のiを参照してしまいます。
左辺をjとした場合は、外側のスコープのiを参照します。難しい。。。

イメージとしてはこんな感じです。

            let i = i;
// 新しいブロック | 内側のブロックのi(内側にiがあるから)

            let j = i;
// 新しいブロック | 外側のブロックのi(内側にiがないから)

let使う時は宣言なのか宣言じゃないのかを意識しないといけないですね。

with文を使う方法

with文でも実現できるよう。

var func_list = [];
for (var i = 1; i < 6; i++) {
  with ({i:i}) {
    func_list.push(function() { return console.log(i); }); 
  }
}

for (var j = 0; j < func_list.length; j++) {
  func_list[j]();
};

letを調べている時に、id:nanto_viさんの記事を見て知りました。

参考: JavaScript でブロックスコープを実現する: Days on the Moon

無名オブジェクトを束縛させて、結果的に無名オブジェクトのプロパティに格納したカウンタ変数も束縛できているという魔法です。

おまけ

Perlも勉強しているので、なんとなくPerlでも書いてみました。

use strict;
use warnings;

my $func_list = [];
for my $i (1..5) {
  {
    my $i = $i;
    push(@{$func_list}, sub { return print $i });
  }
}

for my $j (0..$#{$func_list}) {
  $func_list->[$j]->();
}

Perlだと{}(ブレース)だけで新しい環境を作ることができるので、簡単に実現できます。JavaScriptのlet文に近い感じです。

まとめ

以上の方法をつかって実現できましたが、setTimeout関数だとか、addEventListnerのcallback関数に引数を渡したい時には同じ要領で応用ができます。なので、意外と使う頻度が高かったりします。

ちゃんとまとめたんだから同じことでハマるんじゃないぞ俺。

追記

letは、今後主流の書き方になると思いますと書いたんですが、そう言い切るのはまずいということを知りました。

Twitterにて@taku_eofさんと、@edvakfさんからご指摘いただきました。

@taku_eofさんのつぶやき

http://tinyurl.com/ygbrlww @taiju 氏の記事で let について「サポートするブラウザが増えれば、簡単だしパフォーマンスもいいみたいなので、今後、これが主流になると思われます」と言っているが、ES5 には盛り込まれなかったからなぁ……。

link: http://twitter.com/taku_eof/status/5849137709

@edvakfさんのつぶやき

@taku_eof そうですね。僕もあの書き方はどうかなと思いました。(パフォーマンスを測ってない(?)ところも…)

link: http://twitter.com/edvakf/status/5850725122

英語がわからないなりにECMAScript5の仕様書を読んでみると、letは、将来の予約語としてリストアップされているものの、仕様自体には記載されていないようです。なので主流になるかどうか以前に導入されない可能性もあるので、訂正させていただきました。