2024年9月24日

反復可能なオブジェクト

反復可能な(iterables) オブジェクトは配列の汎化です。これは、for..of ループで任意のオブジェクトを使用できるようにするための概念です。

もちろん、配列は反復可能です。しかし、他にも多くの組み込みオブジェクトがあり、それらも同様に反復可能です。例えば、文字列も反復可能です。後で分かりますが、多くの組み込みの演算子やメソッドはそれらに依存しています。

もしオブジェクトが何かの集合(リスト、セット)を表す場合、for..of はそれをループ処理するのに最適な構文です。それでは、それを動作させる方法を見てみましょう。

Symbol.iterator

自分自身で実際に作ってみると、反復可能(iterables) の概念を簡単に掴む事ができます。

例えば、配列ではありませんが、for..of に適したオブジェクトを持っています。

以下は数値の間隔を表す range オブジェクトです:

let range = {
  from: 1,
  to: 5
};

// for..of が動作するようにしたい:
// for(let num of range) ... num=1,2,3,4,5

range を 反復可能(iterable) にするために (for..of を動作させるために)は、Symbol.iterator (このための特別な組み込みのシンボルです)という名前のメソッドをオブジェクトに追加する必要があります。

  • for..of が始まると、そのメソッドを呼び出します(なければエラーになります)。
  • メソッドは iterator (メソッド next をもつオブジェクト)を返さなければいけません。
  • for..of が次の値を必要とするとき、そのオブジェクトの next() を呼びます。
  • next() の結果は {done: Boolean, value: any} の形式でなければなりません。そして done=true は繰り返しが終わったことを示します。そうでない場合は、value は新しい値である必要があります。

これは range の完全な実装です:

let range = {
  from: 1,
  to: 5
};

// 1. for..of の呼び出しは、最初にこれを呼び出します
range[Symbol.iterator] = function() {

  // ...これは iterator オブジェクトを返します:
  // 2. 以降、for..of はこのイテレータでのみ機能し、次の値を要求します
  return {
    current: this.from,
    last: this.to,

    // 3. for..of ループにより、各繰り返しで next() が呼ばれます
    next() {
      // 4. オブジェクト {done:.., value :...} を返す必要があります
      if (this.current <= this.last) {
        return { done: false, value: this.current++ };
      } else {
        return { done: true };
      }
    }
  };
};

// これで動作します!
for (let num of range) {
  alert(num); // 1, 2, 3, 4, 5
}

反復可能(iterables)の中心的な機能に注目してください。関心の分離です。

  • range 自身は next() メソッドを持っていません。
  • 代わりに、別のオブジェクト、いわゆる “イテレータ” は range[Symbol.iterator]() の呼び出しで生成され、反復を処理します。

従って、反復オブジェクトは反復処理されるオブジェクトから分離されています。

技術的には、コードをシンプルにするためにそれらをマージして、range 自身を イテレータ として使うこともできます。

このようになります:

let range = {
  from: 1,
  to: 5,

  [Symbol.iterator]() {
    this.current = this.from;
    return this;
  },

  next() {
    if (this.current <= this.to) {
      return { done: false, value: this.current++ };
    } else {
      return { done: true };
    }
  }
};

for (let num of range) {
  alert(num); // 1, そして 2, 3, 4, 5
}

今、range[Symbol.iterator]()range オブジェクト自身を返します: next() メソッドを持ち、this.current で現在の反復の状況を覚えています。

欠点は、オブジェクトに対して同時に2つの for..of ループを実行することは不可能だということです。: イテレータ が1つしかないので、オブジェクトは繰り返し状態を共有します。ですが、2つの並列 for-of はたとえ非同期のシナリオにおいてもまれです。

無限のイテレータ

無限の イテレータ もまた実行可能です。例えば、 range.to = Infinity で、range が無限大になります。または、擬似乱数の無限のシーケンスを生成する反復可能なオブジェクトを作ることができます。これもまた役立つことがあります。

next には制限はなく、より多くの値を返すことができますが、これは正常です。

もちろん、このような反復可能なオブジェクトに対する for..of ループはエンドレスですが、break を使っていつでも止めることができます。

文字列は反復可能です

配列と文字列は、最も広く組み込みの iterables で使われています。

文字列では、for..of はその文字をループします:

for (let char of "test") {
  // 文字ごとに1回、計4回実行します。
  alert( char ); // t, 次に e, 次に s, そして t
}

そしてサロゲートペアも正しく動作します!

let str = '𝒳😂';
for (let char of str) {
    alert( char ); // 𝒳, そして次は 😂
}

イテレータを明示的に呼び出す

通常、iterables の内部は外部のコードからは隠れています。for..of ループがあり、それが動作します。それだけを知っておけば問題ありません。

しかし、もう少し深く理解するために、明示的なイテレータの作り方を見てみましょう。

for..of と同じ方法で文字列を反復処理しますが、直接呼び出しをします。このコードは文字列のイテレータを取得し、それを “手動” で呼び出します。:

let str = "Hello";

// for (let char of str) alert(char);
// と同じことをしています

let iterator = str[Symbol.iterator]();

while (true) {
  let result = iterator.next();
  if (result.done) break;
  alert(result.value); // 1つずつ文字を出力します
}

これはほとんど必要とされませんが、for..of よりも処理をよりコントロールできます。例えば、繰り返し処理を分割したい場合: 少し反復してから停止し、別のことをしてから後で再開するような場合に役立ちます。

反復可能(Iterables) と 配列ライク(array-like)

同じように見える2つの正式な用語がありますが、大きく異なるものです。 混乱を避けるためにそれらをよく理解してください。