uhyohyo.net

JavaScript初級者から中級者になろう

十六章第二回 イテレータ

今回はイテレータを紹介します。イテレータ(iterator)とはその名の通り、繰り返し処理できるものです。

そう言われて思い浮かぶのは配列でしょう。例えばfor文なんかでループを回して1つずつ処理するというのは極めてよくあるパターンです。

他にも文字列なども、1文字ずつ処理するという場合はイテレータと見なせます。

ES6ではこれらを抽象化してイテレータとして仕様化されました。

for-of文

for-of文とは、イテレータの中身を1つずつ取り出して処理する文です。その名前から連想される通り、for文やfor-in文と似た使い方ができます。次の例を見てください。

var arr=[0,1,2,3,4];
for(var value of arr){
  console.log(value);
}

この文を実行すると、0から4までが順番に出力されます。この動作は分かりやすいですね。

なお、配列は厳密にはイテレータではありませんが、for-of文は与えられたものをイテレータに変換してから処理します(この表現も厳密には正確ではありませんが)。まだイテレータがどのように定義されるか解説していないので微妙ですが、配列からイテレータへの変換は明らかですね。同様に文字列もイテレータに変換できます。

for(var value of "foobar"){
  console.log(value);
}

この場合は文字列に含まれる文字が1文字ずつ出力されます。

なお、for-of文でもcontinue文やbreak文が使用可能です。

イテレータオブジェクト

イテレータもやはりオブジェクトです。for-of文では内部的に配列をイテレータに変換するためイテレータオブジェクトを直接触ることができませんでしたが、直接触る方法もあります。

配列の場合、valuesメソッドによりイテレータを得ることができます。イテレータはnextメソッドを持ちます。nextメソッドは「次」の値を取得することを意味します。

試しに呼んでみましょう。

var iterator = [1,2,3,4,5].values();
console.log(iterator.next());

nextメソッドの結果はオブジェクトです。この場合、次のような結果になるはずです。

{
  value: 1,
  done: false
}

イテレータのnextメソッドの返り値は、このようにvalueプロパティとdoneプロパティを持つオブジェクトになります。valueプロパティがイテレータが返す値です。doneプロパティは真偽値で、繰り返しが「終了」したかどうかを表します。というのも、イテレータの繰り返しには終わりがある場合があります。配列の場合、配列を最後まで読んでしまったらもう次がないですね。このような場合に、もう次がないのにnextメソッドが呼ばれた場合、イテレータはdoneプロパティがtrueのオブジェクトを返します。一般に、その場合はvalueはundefinedになります。

よって、上のfor-of文の例は次のように書くことができます(実際に処理系の内部で起こっていることは少し違うかもしれませんが)。

var iterator=arr.values();
var result;
while(true){
  result=iterator.next();
  if(result.done){
    break;
  }
  console.log(result.value);
}

なお、配列はvaluesメソッドの他にkeysメソッドとentriesメソッドを持ちます。valuesメソッドは配列の各要素を順番に返すイテレータを作りましたが、keysは添字のイテレータを返します。つまり、単に0,1,2,…という値を返すイテレータになります。当然長さ分だけ繰り返したら終了です。

entriesメソッドは添字と値をペアにした配列を値として返します。これは繰り返すときに配列の値でだけでなく添字もほしいときに使えます。具体的には:

for(var arr of ["foo","bar","baz"].entries()){
  console.log(arr);
}

というコードを実行すると、次のようなログがでます。

[0,"foo"]
[1,"bar"]
[2,"baz"]

イテレータを作る

さて、それでは自分でイテレータを作ってみましょう。実は、nextメソッドを備えたオブジェクトならイテレータになります。

例えば、次のオブジェクトはフィボナッチ数を順に返すイテレータです。

{
  a:1,
  b:0,
  next:function(){
    var n=this.a+this.b, oldb=this.b;
    this.a=oldb, this.b=n;
    return {
      value: oldb,
      done: false
    };
  }
}

このオブジェクトのnextメソッドは常にdoneがfalseのオブジェクトを返しますから、無限イテレータであることが分かります。しかし無限イテレータは無限ループを引き起こす場合もあり、扱いに注意が必要です。

さて、イテレータが出来たのでfor-of文で回してみようとしても、実はエラーが出てうまくいきません。それはなぜかというと、for-of文に渡さなければならないのはイテレータではなくiterableだからです。

iterableとは、イテレータにより順に取り出すことができるオブジェクトのことです。for-of文はiterableを受け取って、それに対応するイテレータを取り出してそれを回して処理します。

最後にfor-of文に配列を渡した例がありましたが、配列もiterableの一種です。上で「配列をイテレータに変換する」という表現が厳密には正確ではないといいましたが、それは配列はあくまでイテレータではなくiterableであり、配列をイテレータに変換というよりはその配列に対応するイテレータを得るといったほうが正しいからです。実際、配列をfor-of文で回している最中に元の配列を変更すると影響がでます。それは、実際回っているイテレータが元の配列を参照しているからであり、イテレータがもとのiterableから切り離されて作られるのではなくあくまでiterableを参照しながら動作していることの証です。強引な例ですが、次のサンプルを実行した場合を考えましょう。

var arr=[1,2,3,4,5];
for(var v of arr){
  console.log(v);
  arr[2]=0;
}

結果は以下になります。1回目のループの時点でarr[2]が0に変更されているからですね。

1
2
0
4
5

これはvaluesメソッドなどを使ってイテレータを得た場合も同様です。これはイテレータ自体がデータを持つのではなく、あくまでiterableがデータを持っているからですね。

以上のように、イテレータそのものではなくiterableを作らないとfor-of文は受け付けてくれません。for-of文以外にもiterableが活躍する場面はありますが、イテレータ単体で使う機会というのはなかなかないでしょう。

さて、どうすればiterableなオブジェクトを作れるかですが、対応するイテレータを取り出す方法を与えればiterableになります。まあ上の説明からして当然ですね。具体的には、@@iteratorメソッドなるものを持つオブジェクトがiterableなオブジェクトです。

@@iteratorというメソッド名らしからぬ名前が出てきましたが、これを解説するにはまだ予備知識が足りません。残念ながら、obj["@@iterator"]=function(){…};なんて書いても意味はありません。自分でiterableを作る方法はまた今度紹介します。

iterableの活用例

iterableが使われるのはfor-of文だけではありません。ここでは他の例を見てみましょう。

Array.fromというメソッドがあります。これは、配列ではないものから配列を作るメソッドです。Object.create(十一章第七回)みたいに、コンストラクタに直接くっついているメソッドですね。

Array.fromは、iterableから配列を作ることができます。イテレータを回して得た値を順番に入れていけばいいわけですね。例えば、文字列もiterableであり、イテレータにより1文字ずつ返されるということを紹介したので、文字列をArray.fromで配列にしてみましょう。

console.log(Array.from("foobar"));

結果は以下のような配列になります。

["f","o","o","b","a","r"]

また、Array.fromには第二引数として関数を指定することができます。これは配列に対するmap操作のような感じで、イテレータから取り出された値を引数にしてその関数が呼び出され、その返り値が配列に入るようになります。イテレータから取り出した値に対して何らかの処理を噛ませてやることができます。例えば先ほどの例で各文字を全て大文字にしたい場合、文字列を大文字して返すメソッドtoUpperCaseを使用して次のように書けます。

console.log(Array.from("foobar",function(char){
  return char.toUpperCase();
}));

この結果は["F","O","O","B","A","R"]になります。まあこの場合はArray.from("foobar".toUpperCase())と書けばいいのであまり関数を使う意味が無かったですね。

なお、関数の第二引数には、Array.prototype.mapと同様に添字が渡されます。また関数呼び出し時のthis値を、Array.fromの第三引数で指定できます。

また、iterableとは関係ない話になりますが、Array.formはiterableだけでなく、array-likeなオブジェクトからも配列を作ることができます。array-likeなオブジェクトとは、"0","1",…という連番のプロパティとlengthプロパティを持ち、配列のように利用できるオブジェクトです。これはDOM関連でよく出現します。例えばNodeList(二章第三回)などです。これらのarray-likeなオブジェクトは、配列と同様に扱うことができるものの、配列ではないので配列が持つ各種のメソッドを使用することができません。また、基本的にはiterableではありません(DOMなどは、JavaScriptにイテレータの概念ができるずっと前から定義されていたし、またDOMはもともとJavaScript専用ではなく、Javaなどでも実装例があります)。Array.fromを用いて配列に変換することでこれらの恩恵を受けることができます。例えば次の例です。

console.log(Array.from({
  "0":"foo",
  "1":"bar",
  "2":"baz",
  length:3
}));

これを実行すると["foo","bar","baz"]という配列が得られ、array-likeなオブジェクトを配列に変換できたことがわかります。

なお、先ほど例に出したNodeListは実はメソッドの戻り値として返されたあとに変化する可能性があります。適当なページで次のコードを実行すると、ps.lengthが変化していることが分かります。

ps = document.getElementsByTagName("p");
console.log(ps.length);
document.body.appendChild(document.createElement("p"));
console.log(ps.length);

Array.fromで配列に変換した場合、もとのオブジェクトとは切り離されるので、その後に元のオブジェクトが変更されたとしても当然反映されません。

補足

上のArray#entriesの例で次のようなコードがあったのに気づいたでしょうか。

for(var arr of ["foo","bar","baz"].entries()){
  console.log(arr);
}

for-of文に渡すのはiterableであってイテレータ自体を渡すのではないと説明したのに、ここでfor-of文にイテレータを渡しています。

実はこのように、配列や文字列などの組み込みのiterableから得られるイテレータはそれ自体iterableになるように作られています(すなわち、@@iteratorなるメソッドが備わっているということです)。当然、対応するイテレータは自分自身になります。

このように、iterableから得られるちゃんとしたイテレータは、それ自体がiterableとなっているのが望ましいですね。先にも述べたように、そのようなiterableの作り方はまた今度です(一六章第四回で説明します)。


今回は以上です。ここではイテレータの例として配列と文字列しかやっていないので、なんだか当たり前でイテレータの凄さが分かりにくいかもしれませんが、イテレータは色々と応用がききます。後々紹介できるといいですね。