uhyohyo.net

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

十六章第二回 イテレータ

このページの最終更新日:

今回はイテレータを紹介します。イテレータ(iterator)とは、ざっくり言うと一連の複数のデータからなるものです。

そう言われて思い浮かぶのは配列でしょう。配列は複数のデータが並んだオブジェクトです。

他にも文字列なども、文字の並びとみなせばイテレータと言えなくもありません。

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

for-of文

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


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

この文を実行すると、0から4までが順番に出力されます。普通のfor文を使うよりも圧倒的に楽ですね。

文字列は文字の並びなので、次のように文字列にfor-of文を使うこともできます。


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

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

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

イテレータオブジェクト

実はさっきは嘘をついていました。配列や文字列は厳密にはイテレータではなく、iterableです。そして、for-of文に渡すべきなのもイテレータではなくiterableです。iterableとは、簡単にいうとイテレータにより順に取り出すことができるものです。for-of文にiterableが渡されると、それに対応するイテレータが作られ、そのイテレータにより値が順番に取り出されるのです。

イテレータもやはりオブジェクトです。実は、配列の場合はvaluesメソッドにより対応するイテレータを得ることができます。

イテレータはnextメソッドを持ちます。nextメソッドは「次」の値を取得することを意味します。

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


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

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


{
  value: 1,
  done: false
}

イテレータのnextメソッドの返り値は、このようにvalueプロパティとdoneプロパティを持つオブジェクトになります。valueプロパティがイテレータが返す値です。今回は、初めてnextメソッドを読んだので配列の最初の値である1となっています。nextメソッドをもう1回呼ぶと次のオブジェクトが返るでしょう。


{
  value: 2,
  done: false
}

このように、nextメソッドを1回呼ぶたびに配列が1つずつ読み進められます。

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

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


var arr=[1, 2, 3, 4, 5];
var iterator=arr.values();
var result;
while(true){
  result=iterator.next();
  if(result.done){
    break;
  }
  console.log(result.value);
}

ただ、現在(2017年10月)配列のvaluesメソッドは多くのブラウザで実装されていないようです。ただ、次に紹介する2つのメソッドは存在します。

それが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の一種です。配列に対応するイテレータは先に説明したようにvaluesメソッドで得られるのでした。実は、普通のイテレータが管理するのは「どこまで読んだか」だけで、データは配列本体にあります。このことは次のサンプルから分かります。


var arr = [3, 1, 4, 1, 5];
// arrのイテレータを作る
var iterator = arr.entries();
// arrを変更
arr[0] = 0;

// 手動でイテレータを回す
var result;
while(true){
  result=iterator.next();
  if(result.done){
    break;
  }
  console.log(result.value);
}

配列からイテレータを作ったあとに配列を変更していますが、イテレータを回すと配列に対する変更の影響を受けていることが分かります。

この例から分かるように、イテレータというのは本来データを保持するオブジェクトからデータを取ってくる役割を持っています。なので、先のフィボナッチ数のような例はあまり適切ではないかもしれません(イテレータ自体がデータを生産しているので)。

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

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

@@iteratorというメソッド名らしからぬ名前が出てきましたが、これを解説するにはシンボルの知識が必要なので後回しにします。少し先取りすると、Symbol.iteratorをプロパティ名とするようなメソッドを作ればよいです。

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にイテレータの概念ができるずっと前から定義されていたので仕方がないことですが)。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になるように作られています。当然、対応するイテレータは自分自身になります。

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


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