プログラミング漫遊記

思ったことや、勉強したことをつらつらと。

【JS】イテレータ(iterator) / 反復可能オブジェクト(iterable object) ってなんだ?

こんにちは。はるぐちです。

最近JavaScriptイテレータについて学習したのでまとめておきたいと思います。 勘違いや誤り等ありましたら、コメントで指摘していただけると助かります🙏

イテレータとは

反復処理などでよく見かけるイテレータ。 わかってるようでわかってないのでまずは言葉の意味を理解するところから。

まずはMDNを見てみます。 イテレーターとジェネレーター - JavaScript | MDN

JavaScript では、イテレーターはシーケンスおよび潜在的には終了時の戻り値を定義するオブジェクトです。

なるほど、よくわからん😅となりますね。

めちゃくちゃざっくりいうと反復処理において、次の値を示すvalueプロパティと,反復処理の終了を表すdoneプロパティの2つのプロパティを返すnextメソッドを実装しているオブジェクトをイテレータと呼びます。

  • イテレータ
    • nextメソッドを持つ
      • 返り値:以下の2つのプロパティを持つオブジェクトを返す
        • value: 反復処理における次の値
        • dole: 反復処理における最後の値が消費されたかどうかを表す。反復処理が終わっていたらtrueになっている

nextメソッドの戻り値を確認してみる

Array.prototype.values()メソッドはイテレータを返すのでnext()メソッドがどのようになっているか、nodeで確かめてみます。

Array.prototype.values() - JavaScript | MDN

> const arr = ['a', 'b', 'c', 'd'];
undefined
> arr.values(); // イテレータが返ってることがわかる
Object [Array Iterator] {}
> const it = arr.values() // イテレータを変数に代入
undefined
> it.next(); // nextメソッドを呼び出すとオブジェクトが返ってくる
{ value: 'a', done: false }
> it.next(); 
{ value: 'b', done: false }
> it.next();
{ value: 'c', done: false }
> it.next();
{ value: 'd', done: false }
> it.next();
{ value: undefined, done: true } // 反復処理が終わったらdone: trueになる
> it.next();
{ value: undefined, done: true } // 反復処理が終わったら何回呼び出しても結果は同じ

イテレータfor ... of文で反復処理を行うことができる

配列からイテレータを取り出してfor ... of文で反復処理を行なってみます。

> const arr = ['foo', 'bar', 'baz'];
> const it = arr.values() // イテレータを代入

> for (const i of it) { // イテレータを反復処理する
// 内部的にイテレータからvalueが取り出される
    console.log(i.toUpperCase()); 
    }
//→FOO
//→BAR
//→BAZ

> it.next()
{ value: undefined, done: true } // 反復処理が終わったのでdone: true

このようにイテレータfor ... of文で反復処理を行うことができます。 (というか`for ... of文がイテレータを処理する実装になっていると言った方が正しいかもしれません。)

反復可能オブジェクト

次に反復可能オブジェクト(iterable object)についてみていきます。

安定のMDN反復処理プロトコル - JavaScript | MDN

反復可能プロトコルによって、 JavaScript のオブジェクトは反復動作を定義またはカスタマイズすることができます。例えば、 for...of 構造の中でどの値がループに使われるかです。一部の組み込み型は既定の反復動作を持つ組み込み反復可能オブジェクトで、これには Array や Map がありますが、他の型 (Object など) はそうではありません。

イテレータの説明よりはわかりやすいかな。😅

これもざっくり説明すると反復可能オブジェクトとはArrayMapStringのようなオブジェクトのことで、イテレータを持っているオブジェクトのことを指します。

(さっきも配列からイテレータを取り出しましたよね)

もう少し厳密にいうと@@iteratorメソッドを実装しているオブジェクトのことで、このメソッドは返り値にイテレータを返します。

@@iteratorメソッドはSymbol.iteratorというプロパティに定義されているので、自作クラスで反復可能オブジェクトを作りたい場合は[Symbol.iterator]メソッドを定義することになります。

  • 反復可能オブジェクト
    • [Symbol.iterator]メソッドを実装しているオブジェクト
      • 返り値はイテレータ(nextメソッドを実装してるオブジェクト)

反復可能オブジェクトはfor ... of文で反復処理ができる

for ... of文は反復可能オブジェクト(iterable object)に対して反復処理ができます。 (イテレータは反復可能オブジェクトでもあるので先ほどのイテレータの例も反復処理が可能だったと考えることができます。)

for...of - JavaScript | MDN

// 以下はStringの反復処理
const str = 'hello world';

for (const c of str) {
  console.log(c.toUpperCase());
}

H
E
L
L
O
 
W
O
R
L
D

自作クラスを反復可能にしてみる

最後に自作クラスを反復可能にしてみます。

やること

Setオブジェクトを再発明したGroupクラス(およびGroupIteratorクラス)を作ります。*1 Groupにはadd, delete, hasメソッドとfromという静的メソッドを用意します。

Setオブジェクトと挙動を合わせるため以下のような要件になっています。

  • Groupクラス
    • add : 引数を1つとり、その値を追加する。重複している値は無視する。
    • delete: 引数を1つとり、その値を削除する
    • has: 引数を1つとり、その値が存在するかどうか確かめる。Booleanを返す。
    • static from: 反復可能なオブジェクトを1つ受け取り、それを反復して生成されたすべての値を含むGroupオブジェクトを作成する。
    • ]symbol.iterator]: イテレータオブジェクトを返すメソッド -GroupIteratorクラス => イテレータオブジェクトを作成するクラス
    • next: {value: ... , done: ...}というオブジェクトを返すメソッド

作成してみた

class Group {
  constructor () {
    this.values = [];
  }

  static from(obj) {
    const group = new Group();
    for (let element of obj) {
      group.add(element);
    }
    return group;
  }

  add(value) {
    if (!this.has(value)) {
      this.values.push(value);
    }
  }

  has(value) {
    return this.values.includes(value);
  }

  delete(value) {
    this.values = this.values.filter(function(ele){
      return ele != value;
    })
  }

  [Symbol.iterator]() {
    return new GroupIterator(this);
  }
}

class GroupIterator {
  constructor(group) {
    this.index = 0;
    this.group = group;
  }

  next() {
    if (this.index === this.group.values.length) return {done: true};

    let result = {value: this.group.values[this.index], done: false};
    this.index++;
    return result;
  }
}

できました。

反復処理してみる

作ったGroupクラスからオブジェクトを作成し、反復処理してみます。

for (let value of Group.from(["a", "b", "c"])) {
  console.log(value);
}
// → a
// → b
// → c

できていますね。 今回で言うとGroupクラスは配列で内部データを表しているのでイテレータを取り出すのはもっと簡単なんですが、わざわざ自分で作ってみて理解が深まりました。

参考

*1:『流麗なJAVASCRIPT』という書籍を参考にしています