LoginSignup
1
1

More than 1 year has passed since last update.

「ざっくり」iteratorとgeneratorを理解する

Last updated at Posted at 2019-11-28

まとめ。

  • 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

たとえば、NodeListfor..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要素が入ったNodeListfor...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要素

一見NodeListHTMLCollectionのようなふるまいですが、実はどちらでもありません。キー名が連番のため配列のように見えるオブジェクト、これを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から生まれたインスタンスを反復可能にしたい時

とかに使えそうですね👀

うまく使いこなせれば、簡潔で一貫性のある見渡しのいいコードが書けるようになるかもしれない重要な仕組みなので、しっかり押さえておきたいです🥒


  1. jQueryが読み込まれている環境前提 

  2. 返り値は0番目の要素オブジェクト 

  3. 返り値は0番目の要素だけを持ったjQueryオブジェクト 

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1