まとめ。
- iterableオブジェクトとは、内部でiteratorオブジェクトを生成して反復可能な状態(
for...of
で回したりできる)にできるオブジェクトのこと - iteratorオブジェクトは、自身が持つgenerator関数から作られる
- generator関数は
[Symbol.iterator]
をキー名にしなければならない - generator関数内では
yield
という新しいキーワードがあり、return
のように処理を中断するが、次に同じ関数がが呼ばれた時に最後に中断した行から処理が再開されるという動きをする - 独自のiterableオブジェクトは簡単に作れる
iterable(イテラブル)とは
ざっくりとしたイメージだと
-
for...of
で回るもの Array
HTMLCollection
NodeList
Map
String
Set
- その他、連番で値が管理されているもの(あくまでもイメージ)
といった具合です。教科書的な説明をするなら、
- 反復可能であるということ
- generator関数を
[Symbol.iterator]
というキーに持っているということ
あたりでしょうか。
オブジェクトがiterableかを判別するには、基本的には[Symbol.iterator]
プロパティが関数になっているかで確認できます。
const arr = [0, 1, 2];
const str = 'hoge';
const obj = {a: 1, b: 2, c: 3};
console.log('Is iterable? => ' + (typeof arr[Symbol.iterator] === 'function'));
console.log('Is iterable? => ' + (typeof str[Symbol.iterator] === 'function'));
console.log('Is iterable? => ' + (typeof obj[Symbol.iterator] === 'function'));
iterator(イテレータ)とは
iterableなオブジェクトの中で使われるオブジェクトのこと。
iterableなオブジェクトが連続して順番に値を返すための機能を持ち、generator関数によって生成されます。
具体的には次のような、next()
メソッドを持つオブジェクトのことです。
type iterator = {
next(): {
done: boolean,
value: any | undefined,
}
}
-
next()
は呼ばれるたびに何か新しい値をvalue
に入れて返します(返す値がなくなったらundefined
) -
done
プロパティは、返す値がなくなった場合true
になります(デフォルトはfalse
)
generator(ジェネレータ)とは
iteratorオブジェクトを作るための関数です。
次のようなルールでgenerator関数を作成できます。通常の関数とかなり振る舞いが違うことに注意してください。
-
function
のあとに*
を書く -
yield
キーワードを使う
yield(イールド、イェルドゥ)とは
- generator関数の中で
return
文の代わりに使う -
yield
キーワードの隣に返したい値を記述する - 一度読み込まれた
yield
は同じiteratorオブジェクトでは決して読まれない
実際に動かしてみる
/** generator関数です */
const generator = function* () {
yield 'h';
yield 'o';
yield 'g';
yield 'e';
};
/** iteratorオブジェクトです */
const iterator = generator(); // generatorからiteratorを生成
// 決められたルールで動かすと、値を順番に返します(反復可能な状態)
console.log(iterator.next().value); // > h
console.log(iterator.next().value); // > o
console.log(iterator.next().value); // > g
console.log(iterator.next().value); // > e
console.log(iterator.next().value, iterator.next().done); // > undefined true
変数iterator
には、前述の通りnext()
メソッドを持つiteratorオブジェクトが入っています。
next()
メソッドは実行されると、次のような構造を持つオブジェクトを返します(1回目)。
{
done: false,
value: "h"
}
-
done
は、すべてのyield
が読み込まれたらtrue
になる -
value
は、next()
が呼ばれた順に先頭からyield
の返り値が入ってくる
最終的には次ような値を返すようになります。
つまり、1度回りきったiteratorオブジェクトは、何も返さなくなるということです。
{
done: true,
value: undefined,
}
一度読み込まれた
yield
は同じiterator
では決して読まれない
このような仕組み上、iteratorオブジェクトは、ループさせるたびに新しいものを生成する必要があります。
ちなみに、iteratorオブジェクト自体もiterableです。
/** iteratorオブジェクトです */
const iterator2 = generator();
console.log([...iterator2]); // > [h, o, g, e]
console.log([...iterator2]); // > []
yield*式
yield*
式は、いっぱいyield
を書かなくてもよくなるものというイメージで最初は良いと思います。
正確には、iteratorが呼ばれ時に参照するオブジェクトを別のiterableオブジェクトで代替する(別のオブジェクトに委任する)ためのものです。
const generator = function* () {
yield* 'hoge'; // Stringはiterable
};
const iterator = generator(); // generatorからiteratorを生成
console.log(iterator.next().value); // > h
console.log(iterator.next().value); // > o
console.log(iterator.next().value); // > g
console.log(iterator.next().value); // > e
console.log(iterator.next().value, iterator.next().done); // undefined true
または
const generator = function* () {
yield* ['h', 'o', 'g', 'e']; // Arrayはiterable
};
const iterator = generator(); // generatorからiteratorを生成
console.log(iterator.next().value); // > h
console.log(iterator.next().value); // > o
console.log(iterator.next().value); // > g
console.log(iterator.next().value); // > e
console.log(iterator.next().value, iterator.next().done); // undefined true
generator関数内の変数は計算内容が継続される?
yield
文はgenerator関数(正確にはiterator)が呼ばれた順番に値を返すのと同時に中断と再開の役割をもっています。
const generator = function* () {
console.log(0);
yield 'h';
console.log(1);
yield 'o';
console.log(2);
yield 'g';
console.log(3);
yield 'e';
console.log(4);
};
const iterator = generator(); // generatorからiteratorを生成
console.log(iterator.next().value); // > 0 h
console.log(iterator.next().value); // > 1 o
console.log(iterator.next().value); // > 2 g
console.log(iterator.next().value); // > 3 e
console.log(iterator.next().value, iterator.next().done); // > 4 undefined true
yield
の間に挟まっているconsole.log()
がiteratorの呼び出しに応じて走っていることから、generator関数内のコードの途中から処理が再開している様子がわかります。
yield
文で中断された計算内容がgenerator関数では保持されているように見えますね。
次の例を見てみましょう。
// ただの関数
const foo = function () {
let index = 0;
while (index < 3) {
return index++; // returnしたあとにインクリメント
}
};
// 何度やっても 0 になる。当たり前。
console.log(foo()); // > 0
console.log(foo()); // > 0
console.log(foo()); // > 0
// generator関数
const foo = function* () {
let index = 0;
while (index < 3) {
yield index++; // yieldしたあとにインクリメント
}
};
const iterator = foo(); // ★
// whileの途中のindexの計算値が帰ってくる
console.log(iterator.next().value); // > 0
console.log(iterator.next().value); // > 1
console.log(iterator.next().value); // > 2
実は実際のところ、generator関数の処理が中断・再開されているように感じているだけなのですが、最初は驚くかもしれません。
勘違いしやすいかもしれないですが、「★」の行でgenerator関数は1度実行され、別のオブジェクトを生成しています。この時点でindex
のインクリメントも、while
文も計算し終わっています。
もし改めてgenerator関数foo()
から新しいiteratorオブジェクトを作れば、それはすでに作ってあるiteratorオブジェクトとは関係がないので再び変数index
の値は0
から始まります。
独自のイテラブルオブジェクトを作る
あとからオブジェクトを反復可能にするには、次のようにgenerator関数を生やします。
const obj = {}; // ただのオブジェクト
obj[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};
const iterator = obj[Symbol.iterator](); // 手動でiteratorを作る
// 手動で呼び出し
console.log(iterator.next().value); // > 1
console.log(iterator.next().value); // > 2
console.log(iterator.next().value); // > 3
console.log(iterator.next().value, iterator.next().done); // > undefined true
// for...ofが初期化から呼び出しまで自動でやってくれている
for (const item of obj) {
console.log(item); // > 1, 2, 3
}
初回のオブジェクト定義の段階でも定義できます。
const obj = {
* [Symbol.iterator]() {
yield 1;
yield 2;
yield 3;
},
hoge: 'piyo',
foo() {
return 100;
}
};
for (const item of obj) {
console.log(item); // > 1, 2, 3
}
class
で定義する場合も同様です。
class Hoge {
* [Symbol.iterator]() {
yield 1;
yield 2;
yield 3;
}
}
for (const item of new Hoge()) {
console.log(item); // > 1, 2, 3
}
Example
たとえば、NodeList
をfor..of
で回してみましょう。
const anchors = document.querySelectorAll('a');
for (const item of anchors) {
console.log(item);
}
普通に回ったと思います。
次に、このNodeList
のiteratorを破壊して別の値が出るようにしてみます。
// 適当な文字列でiteratorを破壊
const anchors = document.querySelectorAll('a');
anchors[Symbol.iterator] = function* () {
yield* ['a', 'b', 'c'];
};
for (const item of anchors) {
console.log(item);
}
a
要素が入ったNodeList
をfor...of
したはずなのに、「a」「b」「c」が出力されました。
yield*
式ではなくyield
で行う場合は次のようにも書けます。
// 適当な文字列でiteratorを破壊
const anchors = document.querySelectorAll('a');
anchors[Symbol.iterator] = function* () {
const arr = ['a', 'b', 'c'];
let index = 0;
while (index < arr.length) {
yield arr[index++];
}
};
for (const item of anchors) {
console.log(item);
}
もともとのNodeList
のiteratorを破壊しつつも、同じ振る舞いをするように書き換えると、次のように書けます(非常に無意味ですが)。
// 標準のiteratorと同じこと
const anchors = document.querySelectorAll('a');
anchors[Symbol.iterator] = function* () {
let index = 0;
// generator関数内の`this`は、最終的にiteratorを持つオブジェクトを参照します。
while (index < this.length) {
yield this[index++];
}
};
for (const item of anchors) {
console.log(item)
}
おまけ1:iterableになったオブジェクトはスプリット構文が使える
不思議な感じがしますが、[object Object]
も配列として分割できます。
スプリット構文もiterableかどうかが重要だったんですね。
const obj = {
*[Symbol.iterator]() {
yield 1;
yield 2;
yield 3;
},
hoge: 'piyo',
foo() {
return 100;
}
};
console.log(...obj); // > 1 2 3
console.log([...obj]); // > (3)[1, 2, 3]
なお、反復可能になったオブジェクトも、次のような分割代入ではもともとの振る舞いをします。
{
...obj, // hoge と foo() が展開される
}
おまけ2:jQueryオブジェクトをiterableにする1
追記(2019/12/10):jQueryの最新バージョンではもともとjQueryオブジェクトがiterableでしたorz
jQueryでは、要素を常に連番で管理します。jQuery内では.get(0)
メソッド2や.eq(0)
メソッド3で個別に取得するのが一般的な方法ですが、ブラケッツ(各括弧)で連番を入れても該当する1つの要素を取ることができます。
console.log($('a')[0]); // 最初のa要素
一見NodeList
やHTMLCollection
のようなふるまいですが、実はどちらでもありません。キー名が連番のため配列のように見えるオブジェクト、これをjQueryオブジェクトと呼んだりします。
jQueryオブジェクトはiterableではありません。for...of
に無理やり入れると、次のようにエラーが吐かれます。
const $a = $('a');
for (const elm of $a) { // > $a is not iterable
console.log(elm);
}
これまで見てきた方法でjQueryオブジェクトにiteratorを生やしてみます。
const $a = $('a');
$a[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};
for (const elm of $a) {
console.log(elm); // > 1 2 3
}
とりあえず回りました。あとは1つ1つyield
が正しくjQueryの持つ要素を返せるようにgeneratorの中身を書き換えて、jQueryの大元に生やすだけです。
$.prototype[Symbol.iterator] = function* () {
let index = 0;
const {length} = this; // thisはjQueryオブジェクトを指します
while (index < length) {
yield this[index++];
}
};
for (const elm of $('a')) {
console.log(elm); // 要素が1つずつコンソールへ出力される
}
無事に回すことができました。とはいえ、これは練習で回せただけだということにご注意ください。
現実的にはjQueryにはほぼ同じ振る舞いをする.each()
メソッドが用意されていますので、jQueryオブジェクトを回したいときにはこちらを利用したほうがいいですね(IEサポートもありますし)。
まとめ
そんな感じで、任意のオブジェクトに自分の意図した値を渡しながら反復処理をさせられるのがiteratorとgeneratorです。
- もっと見る機能
- 重複せずに値を1つずつ返す機能(おみくじとか阿弥陀籤とか?)
- もともとイテラブルなオブジェクトを継承せずに、
class
から生まれたインスタンスを反復可能にしたい時
とかに使えそうですね👀
うまく使いこなせれば、簡潔で一貫性のある見渡しのいいコードが書けるようになるかもしれない重要な仕組みなので、しっかり押さえておきたいです🥒