Firefox 2 の JavaScript における let の使い道

要旨

 Firefox 2 の JavaScript (JavaScript 1.7) の新機能の一つである let を使うと、クロージャーを作るときにありがちなまどろっこしいコードを簡潔に書くことができます。

はじめに

 Firefox 2 では JavaScript 1.7 が使えるようになりました。 generator、 iterator、 destructuring assignment ([a, b] = f();{first: a, second: b} = g(); という形の代入) などの機能が増えています。

 JavaScript 1.7 の新機能の一つが let です。これを見つけて僕は嬉しくなりました。ただ、恥ずかしいことに、今までこの let の機能がほしいと思ったことは何度もあったのに、目の前にあることに気付いていませんでした。 JavaScript 1.7 の新機能は Firefox 2 がリリースされたときに目を通したつもりだったのですが、まあちゃんと見ていなかったのでしょう。 let があると、これまで書きにくかったコードが簡単に書けるようになる局面があります。

注意

 ここで紹介するのは今すぐ使えるハウツーではありません。現時点ではこの構文が使えるウェブブラウザーは Firefox 2 くらいしかないので、一般的なウェブページで使うのはお勧めしません。今すぐに使えるのは、 Firefox 2 を対象とする Firefox 拡張機能や、クライアントが Firefox 2 であるようなイントラネット環境でのアプリケーションを書いている人だけです。

 ただ、プログラムを書く人に、今すぐプログラムを書く役には立たなくても、「こういう構文があるとこんな風に役に立つのか。ふーん」と思ってもらえたら嬉しいなと。

let のない世界

 僕は以前次のようなプログラムを書いて、うまく動作せず困った経験があります。

プログラム 1 (実行例)

 1  var list = document.getElementById("list");
 2  var i;
 3  for (i = 1; i <= 5; i++)
 4  {
 5      var item = document.createElement("LI");
 6      item.appendChild(document.createTextNode("Item " + i));
 7      item.onclick = function (ev) {
 8          alert("Item " + i + " is clicked.");
 9      };
10      list.appendChild(item);
11  }

 ここで list というのは HTML 文書中の UL 要素に付けられた ID だとします。プログラムは例えば BODY 要素の onload から呼ばれていると考えてください。以下のプログラムでも同様です。

 このコードを実行すると、 Item 1 から Item 5 まで 5 個の項目を持つリストが表示されますが、どれをクリックしても「Item 6 is clicked.」と表示されてしまいます。

 無名関数の中で参照している i は当然コード 2 行目で宣言されている i です。この i の値は最初 undefined で、その後 1, 2, 3, 4, 5, 6 と変わっていきます。無名関数を 5 個作った後、 for ループを抜けたときには i の値は 6 になっています。このため、リスト項目をクリックすると「Item 6 is clicked.」と表示されるのです。無名関数の中で参照している変数に対して、破壊的代入 (JavaScript での代入は当然すべて破壊的代入です) を行うから、予期したように動作しないのです。

 プログラムを次のように変えても問題は解決しません。

プログラム 2 (実行例)

 1  var list = document.getElementById("list");
 2  var i;
 3  for (i = 1; i <= 5; i++)
 4  {
 5      var item = document.createElement("LI");
 6      item.appendChild(document.createTextNode("Item " + i));
 7      var j = i;
 8      item.onclick = function (ev) {
 9          alert("Item " + j + " is clicked.");
10      };
11      list.appendChild(item);
12  }

 7 行目で宣言された変数 j のスコープは 12 行目で終わりますが、べつにループを回るたびに新しい変数が作られるわけではないので、どのリスト項目をクリックしても同じ結果になることに変わりはありません (このプログラムではどれでも「Item 5 is clicked.」と表示されます)。

 この問題を解決するには、無名関数を作るたびに別の変数を用意してやる必要があります。 Firefox 1.5.x 以前の JavaScript (ECMAScript 言語仕様第 3 版、 JavaScript 1.6) では、新しい変数を用意するためには関数呼び出しをする必要がありました (6 月 6 日追記: これは間違いでした。 with 文を使っても実現できます)。なので、こういうプログラムにする必要がありました。

プログラム 3 (実行例)

 1  function createHandler(j)
 2  {
 3      return function (ev) {
 4          alert("Item " + j + " is clicked.");
 5      };
 6  }
 7
 8  var list = document.getElementById("list");
 9  var i;
10  for (i = 1; i <= 5; i++)
11  {
12      var item = document.createElement("LI");
13      item.appendChild(document.createTextNode("Item " + i));
14      item.onclick = createHandler(i);
15      list.appendChild(item);
16  }

 createHandler 関数を無名関数にしてもよいので、下のようにも書けますが、事情を知らない人には何が何だかわからないプログラムになっているでしょうね。

プログラム 4 (実行例)

 1  var list = document.getElementById("list");
 2  var i;
 3  for (i = 1; i <= 5; i++)
 4  {
 5      var item = document.createElement("LI");
 6      item.appendChild(document.createTextNode("Item " + i));
 7      item.onclick = function (j) {
 8          return function (ev) {
 9              alert("Item " + j + " is clicked.");
10          };
11      } (i);
12      list.appendChild(item);
13  }

 さらに、今 j と書いている変数にいちいち i とは別の名前を使う理由もないので両方とも i と書くと、ますますミステリアスになります。べつにわざとわかりにくくしているのではないのですが。

let の威力

 Firefox 2 (JavaScript 1.7) で新たに導入された let 文は、次のような構文で使います。

let (〈変数名〉 = 〈式〉 [, 〈変数名〉 = 〈式〉 [, …]])
    〈文〉

 この文を実行すると、新たに局所変数を作り、 〈式〉 を評価して結果を代入し、その局所変数が 〈変数名〉 の名前で参照できる状態で 〈文〉 を実行します。なお、外側で同じ名前の変数があっても無関係になります。

 この構文を使うと、上のプログラムは次のように簡潔に書けます。

プログラム 5 (実行例)

 1  var list = document.getElementById("list");
 2  var i;
 3  for (i = 1; i <= 5; i++)
 4  {
 5      var item = document.createElement("LI");
 6      item.appendChild(document.createTextNode("Item " + i));
 7      let (j = i)
 8      {
 9          item.onclick = function (ev) {
10              alert("Item " + j + " is clicked.");
11          };
12      }
13      list.appendChild(item);
14  }

 これはいいですね。なお、 JavaScript 1.7 の新機能を使うには、 SCRIPT 要素などの type 属性を「application/javascript;version=1.7」とする必要があります。

 なお、 for 文でループを回す限り、 j = i のような一見無駄に見える代入を防いで一つの変数だけを使って同じことを実現するのは不可能だと思います。

let 式と let 定義

 JavaScript 1.7 では let 文以外にも let 式と let 定義という新しい構文が用意されています。どれも新しい局所変数を作るための構文です。説明は上でもリンクを張った Mozilla Developer Center を参照していただくことにして、上のプログラムは let 式や let 定義を使っても書けるので、そのサンプルだけ示しておきます。

 let 式を使うと次のようになります。

プログラム 6 (実行例)

 1  var list = document.getElementById("list");
 2  var i;
 3  for (i = 1; i <= 5; i++)
 4  {
 5      var item = document.createElement("LI");
 6      item.appendChild(document.createTextNode("Item " + i));
 7      item.onclick = let (j = i) function (ev) {
 8          alert("Item " + j + " is clicked.");
 9      };
10      list.appendChild(item);
11  }

 let 定義を使うと下のようになります。プログラム 2 と見比べると var を 1 個所 let に書き換えただけでほとんど同じですが、こちらのプログラムでは変数 j がループを回るたびに新しく作られることに注意してください。

プログラム 7 (実行例)

 1  var list = document.getElementById("list");
 2  var i;
 3  for (i = 1; i <= 5; i++)
 4  {
 5      var item = document.createElement("LI");
 6      item.appendChild(document.createTextNode("Item " + i));
 7      let j = i;
 8      item.onclick = function (ev) {
 9          alert("Item " + j + " is clicked.");
10      };
11      list.appendChild(item);
12  }

関連ページ

 nanto_vi さんのブログ「Days on the Moon」の記事「JavaScript でブロックスコープを実現する」で let 構文が詳しく解説されています。 let でも内部関数でもなく with 文を使う方法もあるということを知りました。同じ「Days on the Moon」の「JavaScript 1.7 の新機能」もお勧めです。

2007 年 6 月 2 日公開、 2007 年 6 月 6 日関連ページ追加、間違いを修正。著者: fcp / このサイトについて