JavaScript の イテレータ を極める!

  • 272
    いいね
  • 1
    コメント

ECMAScript 6(2015年6月に公開され、今もなお比較的新しい JavaScript)の大目玉である イテレータジェネレータ。なかなかに複雑で巨大な仕組みになっていてややこしいです。
そこで今回は イテレータ を、順を追って理解できるように解説したいと思います。

また、実用的なサンプルを「3. 実用サンプル」に示しました。
初めにこちらを見て、何ができるのかを知ってから読み始めるのもオススメです。

(2017年3月現在、オープンなページでの使用はまだ避けたほうがいいかもしれませんが、実装は確実に進んでいます。ECMAScript 6 compatibility table

1. ことばの定義

1.1. イテレータ (Iterator)、イテレータリザルト (Iterator Result) とは

イテレータ とは、「順番にイテレータリザルトを取り出すことのできるオブジェクト」のことです。
具体的に示すと、以下の2点を満たすオブジェクトを、イテレータ と言います。

  • .next() メソッドを持つ こと
  • .next() を実行すると イテレータリザルト を返す こと

つまり、以下のコードにおいて、iteratorイテレータ です。

イテレータの定義、イテレータリザルトの定義
var iterator = {}; // イテレータ
iterator.next = function(){
    var iteratorResult = { value: 42, done: false }; // イテレータリザルト
    return iteratorResult;
};

ここで、上のコードで iteratorResult という イテレータリザルト が出てきました。
見ての通り、イテレータリザルト はオブジェクトであり、.value プロパティと .done プロパティを持っています。
それぞれのプロパティの役割は以下のようになっています。

  • .value プロパティ は、イテレータから取り出した 値(アイテム)
  • .done プロパティ は、イテレータから値を順番に取り出し終えたかどうかの 真偽値

1.2. イテラブル (Iterable) とは

イテレータを持つオブジェクト」のことです。
具体的に示すと、以下のことを満たすオブジェクトを、イテラブル であると言います。

  • [Symbol.iterator]() メソッドを実行すると イテレータ を返す こと

つまり、以下のコードにおいて、obj は イテラブル です。

イテラブルなオブジェクトの定義
var iterator = {}; // イテレータ
iterator.next = function(){
    var iteratorResult = { value: 42, done: false }; // イテレータリザルト
    return iteratorResult;
};

var obj = {}; // イテラブルなオブジェクト
obj[Symbol.iterator] = function(){
    return iterator;
};

イテラブルであるオブジェクトのことを イテラブルなオブジェクト とも言います。

1.3.【まとめ】イテラブル、イテレータ、イテレータリザルト

名称 説明 持っているメソッド/プロパティ
イテラブル (Iterable) なオブジェクト イテレータを持つオブジェクト [Symbol.iterator]()
イテレータ (Iterator) 順番にイテレータリザルトを取り出すことのできるオブジェクト .next()
イテレータリザルト (Iterator Result) 取り出した値や、取り出し終えたかどうかの真偽値を持つオブジェクト .value, .done

この関係を図に示すと、下のようになります。
イテラブル、イテレータ、イテレータリザルト

2. イテレータ を使う

定義が理解できたところで、実際に使ってみましょう。

2.1. まずは イテラブルなオブジェクト を作る

まずは簡単な例として、"1~10の数を順番に取り出せるイテレータを持つ、イテラブルなオブジェクト" を作成してみます。

イテラブルなオブジェクトを準備する
var obj = {}; // イテラブルなオブジェクト
obj[Symbol.iterator] = function(){
    var iterator = {}; // イテレータ
    var count = 1;
    iterator.next = function(){
        var iteratorResult = (count <= 10)
            ? { value: count++,   done: false }
            : { value: undefined, done: true };
        return iteratorResult; // イテレータリザルト
    };
    return iterator;
};

現在の値が変数 count に保持されていて、.next() を実行するごとにカウントアップして .value で返す 仕組みになっています。
count が10を超えたら、.donetrue にし、値を順番に取り出し終えた ことを示します。
これで "1~10の数を順番に取り出せるイテレータを持つ、イテラブルなオブジェクト" が準備できました。

2.2. 次に イテレータ から順番に値を取り出す

先ほど作った イテレータ から順番に値を取り出して、コンソールに出力してみます。
.next() を用いると イテレータリザルト が取り出せる 性質を利用します。

イテレータから値を順番に取り出す
var obj = {}; // イテラブルなオブジェクト
obj[Symbol.iterator] = function(){
    var iterator = {}; // イテレータ
    var count = 1;
    iterator.next = function(){
        var iteratorResult = (count <= 10)
            ? { value: count++,   done: false }
            : { value: undefined, done: true };
        return iteratorResult; // イテレータリザルト
    };
    return iterator;
};

var iterator = obj[Symbol.iterator](); // イテラブルなオブジェクトからイテレータを取得する
var iteratorResult;
while(true){
    iteratorResult = iterator.next(); // 順番に値を取りだす
    if(iteratorResult.done) break; // 取り出し終えたなら、break
    console.log(iteratorResult.value); // 値をコンソールに出力
}
/*
  1
  2
  3
  ...
  10
*/

これで、イテレータ から順番に値を取り出すことができました。
しかし、このコードは ECMAScript 5 でも書けるコードであり、書き方も無駄にややこしく、あまりメリットを感じられません。
それではなぜ イテレータ が便利なのか、それは for(v of iterable) という構文を使えば、もっと楽に値を取り出せるからです。

2.3. もっと楽に イテレータ から値を取り出す

イテレータ から値を取り出すのに用意されている便利な構文が、for(v of iterable) です。

for-ofを使ってイテレータから値を順番に取り出す
var obj = {}; // イテラブルなオブジェクト
obj[Symbol.iterator] = function(){
    var iterator = {}; // イテレータ
    var count = 1;
    iterator.next = function(){
        var iteratorResult = (count <= 10)
            ? { value: count++,   done: false }
            : { value: undefined, done: true };
        return iteratorResult; // イテレータリザルト
    };
    return iterator;
};

for(var v of obj) console.log(v);
/*
  1
  2
  3
  ...
  10
*/

この for(v of iterable) という構文は、以下のような処理を順に実行しています。

  1. まず iterator = iterable[Symbol.iterator]() を実行して、イテレータ を取得する
  2. 次に iteratorResult = iterator.next() を実行して、イテレータリザルトを取り出す
  3. もし iteratorResult.done == true なら、取り出し終えたので終了する。そうでないなら 4. に進む
  4. v = iteratorResult.value を代入して、文(console.log(v))を実行する
  5. 2. に戻る

この処理、2.2. で書いたコードとほぼ同じ処理です。
つまり、2.2. の長々しいコードを for(v of iterable) という短いコードだけで実現できるのです。

2.4. 初めから用意されている イテラブルなオブジェクト

2.3. で紹介した for(v of iterable) を使うことで、だいぶスマートにコードを書くことができました。
しかし、イテラブルなオブジェクト を定義する部分が非常に長くなっています。
実は、自分で イテラブルなオブジェクト を定義せずとも、JavaScript ですでに用意してくれている イテラブルなオブジェクト があります。

2.4.1. 配列(Array)関連

まず、配列そのものが イテラブルなオブジェクト です

配列そのものがイテラブルなオブジェクト
var obj = ["A", "B", "C"]; // イテラブルなオブジェクト
for(var v of obj) console.log(v);
/*
  "A"
  "B"
  "C"
*/

確認のため、配列が今まで上げてきたような性質を持つイテラブルなオブジェクトかどうか見てみます。

配列がイテラブルなオブジェクトかどうかの確認
var obj = ["A", "B", "C"]; // イテラブルなオブジェクト
var iterator = obj[Symbol.iterator]();
console.log(typeof iterator); // "object"。確かにイテレータを取得できている
console.log(iterator.next()); // { value: "A", done: false }

また、配列には .keys()メソッド があり、これは配列のキーを順番に取り出す イテレータ を取得できます。

配列の.keys()メソッド
var obj = ["A", "B", "C"]; // イテラブルなオブジェクト
for(var v of obj.keys()) console.log(v);
/*
  0
  1
  2
*/

また、配列には .entries()メソッド というものもあり、これは配列のキーと値がセットになった配列を順番に取り出す イテレータ を取得できます。

配列の.entries()メソッド
var obj = ["A", "B", "C"]; // イテラブルなオブジェクト
for(var v of obj.entries()) console.log(v);
/*
  [0, "A"]
  [1, "B"]
  [2, "C"]
*/

2.4.2. 文字列(String)

文字列オブジェクトも イテラブルなオブジェクト です
文字列の先頭から1文字ずつ文字を取り出すことができます。

文字列オブジェクトはイテラブルなオブジェクト
var str = "あいう";
for(var v of str) console.log(v);
/*
  "あ"
  "い"
  "う"
*/

確認のため、文字列も今まで上げてきたような性質を持つイテラブルなオブジェクトかどうか見てみます。

配列がイテラブルなオブジェクトかどうかの確認
var str = "あいう";
var iterator = str[Symbol.iterator]();
console.log(typeof iterator); // "object"
console.log(iterator.next()); // { value: "あ", done: false }

2.4.3. イテレータ(Iterator)

実は、JavaScript で用意されている イテレータ は、それ自身がイテラブルなオブジェクトなのです。
ゆえに、[Symbol.iterator]() メソッドを実行すると、自分自身を返します

イテレータ自身がイテラブルなオブジェクト
var obj = ["A", "B", "C"]; // イテラブルなオブジェクト
var iterator = obj[Symbol.iterator](); // イテレータを取得する
for(var v of iterator) console.log(v); // for-of にイテレータを渡す
/*
  "A"
  "B"
  "C"
*/

console.log(iterator === iterator[Symbol.iterator]()); // true

2.4.4. ジェネレータ

ジェネレータ関数から生成される ジェネレータ は、イテラブルなオブジェクトであり、イテレータ でもあります。
ジェネレータ はこれまた非常に盛りだくさんな仕組みになっていますので、別の記事で解説します。
Qiita: JavaScript の ジェネレータ を極める!

ジェネレータはイテラブルなオブジェクト
function* gfn(n){
    while(n < 100){
        yield n;
        n *= 2;
    }
}
var gen = gfn(3);
for(var v of gen) console.log(v);
/*
  3
  6
  12
  24
  48
  96
*/

2.4.5. その他もろもろ

その他もイテラブルなオブジェクトがいろいろあります。

Arguments

Argumentsはイテラブルなオブジェクト
// Firefox 40 ではこのサンプルのみ動作しません
function func(){
    for(var v of arguments) console.log(v);
}
func(42, "あ", true);
/*
  42
  "あ"
  true
*/

TypedArray

TypedArrayはイテラブルなオブジェクト
var view = new Uint8Array([0, 1, -1]);
for(var v of view) console.log(v);
/*
  0
  1
  255
*/

Map

Mapはイテラブルなオブジェクト
var map = new Map([[0, "Zero"], [{}, "Object"], [[], "Array"]]);
for(var v of map) console.log(v);
/*
  [0, "Zero"]
  [{}, "Object"]
  [[], "Array"]
*/

Set

Setはイテラブルなオブジェクト
var set = new Set([0, {}, []]);
for(var v of set) console.log(v);
/*
  0
  {}
  []
*/

2.5. もっとある!イテレータ の利用法

2.3. では、for(v of iterable) という構文で イテレータ を利用しました。
実は、イテレータ を利用する方法はこの他にもたくさんあります。

2.5.1. 配列

[...iterable] という構文です。
iterable からは順番に値が取り出されて、個数分の要素が該当部分に入るような配列を作成できます。

イテレータを配列リテラルに利用する
var ary = [0, "A", false];
var str = "あいう";
var connectedAry = [...ary, ...str];
console.log(connectedAry);
/*
  [0, "A", false, "あ", "い", "う"]
*/

また、Array.from(iterable) という構文でも同様のことが可能です。

イテレータをArray.from()に利用する
var str = "あいう";
var ary = Array.from(str);
console.log(ary); // ["あ", "い", "う"]

2.5.2. 引数渡し

func(...iterable) という構文です。
iterable からは順番に値が取り出されて、個数分の引数が該当部分に入るように関数を実行します。

イテレータを引数渡しに利用する
var nums = [112, 105, 121, 111];
console.log( Math.max(...nums) ); // 121
console.log( String.fromCharCode(...nums) ); // "piyo"

2.5.3. 分割代入

[a, b, c] = iterable という構文です。
iterable からは順番に値が取り出されて、左辺の変数に順番に代入されます。

イテレータを分割代入に利用する
var [a, b, c] = "ひよこ";
console.log(c+b+a); // "こよひ"

2.5.4. Map, Set, WeakMap, WeakSet

new Map(iterable), new Set(iterable), new WeakMap(iterable), new WeakSet(iterable) という構文です。
それぞれ iterable からは順番に値が取り出されて、キーや値を指定することができます。

イテレータをSetに利用する
var set = new Set("あいうあお");
console.log(set); // Set {"あ", "い", "う", "お"}
イテレータをMapに利用する
var map = new Map(["A", "B", "C"].entries());
console.log(map); // Map {0 => "A", 1 => "B", 2 => "C"}

2.6.【まとめ】イテラブルなオブジェクト の種類、利用法

以上の内容をまとめるとともに、対応ブラウザバージョンを併記しました。
ただし、組み合わせによっては動かないものも稀にありますので、詳細は ECMAScript 6 compatibility table を参照してください。

2.6.1. イテラブルなオブジェクト の種類

2.4.1. で上げたように、配列 ["A", "B", "C"] は イテラブルなオブジェクト です。
今まで紹介した イテラブルなオブジェクト をすべて下の表にまとめました。

コード例 記事参照 IE Fx GC 備考
自分で作る 2.1. Edge
配列 ["A", "B", "C"] 2.4.1.
配列の.keys(), .entries() ["A", "B", "C"].entries() 2.4.1. Edge それ自身がイテレータ
文字列 "あいう" 2.4.2.
イテレータ 2.4.3. Edge それ自身がイテレータ
ジェネレータ (function*(){})() 2.4.4. Edge13 それ自身がイテレータ
Arguments (function(){arguments/*←コレ*/})() 2.4.5. ○ ※A
TypedArray new Uint8Array([0, 1, -1]) 2.4.5. IE10
Map new Map() 2.4.5. Edge
Set new Set() 2.4.5. Edge

2017年3月現在の最新ブラウザ:Edge14, Fx52, GC56
※A: ただしイテラブルではありません → イテラブルになりました(Fx46~47?)

2.6.2. イテラブルなオブジェクト の利用法

2.3. で上げたように、for-of文は イテラブルなオブジェクト を利用する方法の一つです。
今まで紹介した イテラブルなオブジェクト を利用する方法を、すべて下の表にまとめました。

コード例 記事参照 IE Fx GC
コードを自分で書く 2.2. Edge
for-of 文 for(v of iterable) 2.3. Edge
配列リテラル [...iterable] 2.5.1. Edge 46
Array.from Array.from(iterable) 2.5.1. Edge 45
引数渡し func(...iterable) 2.5.2. Edge 46
分割代入 [a, b, c] = iterable 2.5.3. Edge14 49
Map new Map(iterable) 2.5.4. Edge
Set new Set(iterable) 2.5.4. Edge
WeakMap new WeakMap(iterable) 2.5.4. Edge
WeakSet new WeakSet(iterable) 2.5.4. Edge

2017年3月現在の最新ブラウザ:Edge14, Fx52, GC56

3. 実用サンプル

今までの内容を応用したものです。

配列の要素をひとつひとつコンソールに出力する
var ary = [0, 5, 9, 2, 7];
for(var v of ary) console.log(v);
/*
  0
  5
  9
  2
  7
*/
配列をコピーする
var ary0 = [1, 2, 3];
var ary1 = [...ary0];
console.log(ary0.join() === ary1.join()); // true
console.log(ary0 === ary1); // false
配列の最初の要素を代入する
var ary = ["A", "B", "C"];
var [first] = ary;
console.log(first); // "A"
文字列の最初の文字を代入する
var str = "ABC";
var [first] = str;
console.log(first); // "A"
配列から重複した値を削除する
var ary = [0, 5, 9, 0, 2, 5];
var uniqueAry = [...new Set(ary)];
console.log(uniqueAry); // [ 0, 5, 9, 2 ]
.apply()を使わずに関数に可変長の引数を渡す
var nums = [112, 105, 121, 111];
console.log( Math.max(...nums) ); // 121
console.log( String.fromCharCode(...nums) ); // "piyo"
マッチした文字と、部分マッチした文字を一気に代入する
var [all, part] = "abcde".match(/ab(.)de/)
console.log(all, part); // "abcde", "c"

4. 参考

ECMAScript 2015 Language Specification – ECMA-262 6th Edition
Iterators and generators - JavaScript | MDN
イテレータについて - JS.next
十六章第二回 イテレータ — JavaScript初級者から中級者になろう — uhyohyo.net
ECMAScript 6 compatibility table