2022年7月15日

反復可能オブジェクト

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

もちろん、配列は反復可能です。しかし、同様に反復可能な他の多くの組み込みオブジェクトがあります。例えば、文字列も反復可能です。

オブジェクトが技術的には配列ではないが、何かのコレクション(リスト、セット)を表す場合、for..ofはそれをループ処理するための優れた構文です。そのため、どのように動作させるかを見てみましょう。

Symbol.iterator

独自の反復可能オブジェクトを作成することで、反復可能オブジェクトの概念を簡単に理解できます。

たとえば、配列ではないが、for..ofに適したオブジェクトがあるとします。

数値の範囲を表すrangeオブジェクトなどです。

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

// We want the for..of to work:
// for(let num of range) ... num=1,2,3,4,5

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

  1. for..ofが開始すると、このメソッドを一度呼び出します(見つからない場合はエラーが発生します)。このメソッドは、イテレータを返す必要があります。イテレータとは、nextメソッドを持つオブジェクトのことです。
  2. 以降、for..of返されたオブジェクトのみを処理します。
  3. for..ofが次の値を要求すると、そのオブジェクトでnext()を呼び出します。
  4. next()の結果は{done: Boolean, value: any}という形式でなければなりません。ここで、done=trueはループが終了したことを意味し、そうでない場合はvalueが次の値になります。

コメント付きのrangeの完全な実装を以下に示します。

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

// 1. call to for..of initially calls this
range[Symbol.iterator] = function() {

  // ...it returns the iterator object:
  // 2. Onward, for..of works only with the iterator object below, asking it for next values
  return {
    current: this.from,
    last: this.to,

    // 3. next() is called on each iteration by the for..of loop
    next() {
      // 4. it should return the value as an object {done:.., value :...}
      if (this.current <= this.last) {
        return { done: false, value: this.current++ };
      } else {
        return { done: true };
      }
    }
  };
};

// now it works!
for (let num of range) {
  alert(num); // 1, then 2, 3, 4, 5
}

反復可能オブジェクトの中核となる機能、つまり懸念事項の分離に注意してください。

  • range自体にはnext()メソッドがありません。
  • 代わりに、range[Symbol.iterator]()の呼び出しによっていわゆる「イテレータ」オブジェクトが作成され、そのnext()が反復のための値を生成します。

したがって、イテレータオブジェクトは、それが反復処理するオブジェクトとは別個です。

技術的には、これらをマージして、コードを簡素化するために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, then 2, 3, 4, 5
}

これでrange[Symbol.iterator]()rangeオブジェクト自体を返します。これは必要なnext()メソッドを持ち、this.currentに現在の反復の進行状況を記憶しています。より短いですか?はい。そして、場合によってはそれも問題ありません。

欠点は、オブジェクトに対して2つのfor..ofループを同時に実行できなくなることです。イテレータはオブジェクト自体のみであるため、それらは反復状態を共有します。しかし、非同期シナリオであっても、2つの並列for-ofはまれなことです。

無限イテレータ

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

nextに制限はありません。より多くの値を返すことができます。それは普通のことです。

もちろん、そのような反復可能オブジェクトに対するfor..ofループは無限ループになります。しかし、breakを使用していつでも停止できます。

文字列は反復可能

配列と文字列は、最も広く使用されている組み込みの反復可能オブジェクトです。

文字列の場合、for..ofは文字をループ処理します。

for (let char of "test") {
  // triggers 4 times: once for each character
  alert( char ); // t, then e, then s, then t
}

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

let str = '𝒳😂';
for (let char of str) {
    alert( char ); // 𝒳, and then 😂
}

イテレータの明示的な呼び出し

より深い理解のために、イテレータを明示的に使用する方法を見てみましょう。

for..ofとまったく同じ方法で文字列を反復処理しますが、直接呼び出しを行います。このコードは文字列イテレータを作成し、そこから「手動で」値を取得します。

let str = "Hello";

// does the same as
// 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); // outputs characters one by one
}

これはめったに必要ありませんが、for..ofよりもプロセスをより詳細に制御できます。たとえば、反復プロセスを分割できます。少し反復処理してから停止し、別の処理を行い、後で再開します。

反復可能オブジェクトと配列様オブジェクト

2つの公式用語は似ていますが、大きく異なります。混乱を避けるために、それらをよく理解してください。

  • 反復可能オブジェクトは、上記のようにSymbol.iteratorメソッドを実装するオブジェクトです。
  • 配列様オブジェクトは、インデックスとlengthを持つオブジェクトであり、配列のように見えます。

ブラウザやその他の環境でJavaScriptを実用的なタスクに使用する場合、反復可能オブジェクトまたは配列様オブジェクト、あるいはその両方であるオブジェクトに出会うことがあります。

たとえば、文字列は反復可能(for..ofで動作する)であり、配列様(数値インデックスとlengthを持つ)でもあります。

しかし、反復可能オブジェクトは配列様オブジェクトではない場合があります。そしてその逆もまた然りです。

たとえば、上記の例にあるrangeは反復可能ですが、インデックス付きのプロパティとlengthがないため、配列様ではありません。

そして、配列様オブジェクトだが反復可能ではないオブジェクトを以下に示します。

let arrayLike = { // has indexes and length => array-like
  0: "Hello",
  1: "World",
  length: 2
};

// Error (no Symbol.iterator)
for (let item of arrayLike) {}

反復可能オブジェクトと配列様オブジェクトは通常配列ではありませんpushpopなどは持っていません。このようなオブジェクトがあり、配列のように操作したい場合、これは非常に不便です。たとえば、配列メソッドを使用してrangeを操作したいとします。どのようにすれば達成できますか?

Array.from

反復可能オブジェクトまたは配列様オブジェクトを受け取り、「実際の」Arrayを作成する汎用メソッドArray.fromがあります。その後、その配列メソッドを呼び出すことができます。

たとえば

let arrayLike = {
  0: "Hello",
  1: "World",
  length: 2
};

let arr = Array.from(arrayLike); // (*)
alert(arr.pop()); // World (method works)

(*)行のArray.fromはオブジェクトを受け取り、それが反復可能オブジェクトまたは配列様オブジェクトかどうかを調べ、新しい配列を作成してすべてのアイテムをコピーします。

反復可能オブジェクトでも同じことが起こります。

// assuming that range is taken from the example above
let arr = Array.from(range);
alert(arr); // 1,2,3,4,5 (array toString conversion works)

Array.fromの完全な構文では、オプションの「マッピング」関数も提供できます。

Array.from(obj[, mapFn, thisArg])

オプションの第2引数mapFnは、配列に追加する前に各要素に適用される関数にすることができ、thisArgを使用すると、そのthisを設定できます。

たとえば

// assuming that range is taken from the example above

// square each number
let arr = Array.from(range, num => num * num);

alert(arr); // 1,4,9,16,25

ここでは、Array.fromを使用して文字列を文字の配列に変換しています。

let str = '𝒳😂';

// splits str into array of characters
let chars = Array.from(str);

alert(chars[0]); // 𝒳
alert(chars[1]); // 😂
alert(chars.length); // 2

str.splitとは異なり、文字列の反復可能な性質に依存しているため、for..ofと同様に、サロゲートペアでも正しく動作します。

技術的には、ここでは以下と同じことを行っています。

let str = '𝒳😂';

let chars = []; // Array.from internally does the same loop
for (let char of str) {
  chars.push(char);
}

alert(chars);

…しかし、より短いです。

サロゲートペアに対応したsliceを構築することもできます。

function slice(str, start, end) {
  return Array.from(str).slice(start, end).join('');
}

let str = '𝒳😂𩷶';

alert( slice(str, 1, 3) ); // 😂𩷶

// the native method does not support surrogate pairs
alert( str.slice(1, 3) ); // garbage (two pieces from different surrogate pairs)

まとめ

for..ofで使用できるオブジェクトは、反復可能オブジェクトと呼ばれます。

  • 技術的には、反復可能オブジェクトはSymbol.iteratorという名前のメソッドを実装する必要があります。
    • obj[Symbol.iterator]()の結果は、イテレータと呼ばれます。これは、さらなる反復プロセスを処理します。
    • イテレータはnext()という名前のメソッドを持っている必要があり、{done: Boolean, value: any}というオブジェクトを返します。ここでdone:trueは反復プロセスの終了を示し、そうでない場合はvalueが次の値です。
  • Symbol.iteratorメソッドはfor..ofによって自動的に呼び出されますが、直接行うこともできます。
  • 文字列や配列などの組み込みの反復可能オブジェクトもSymbol.iteratorを実装しています。
  • 文字列イテレータはサロゲートペアを認識しています。

インデックス付きのプロパティとlengthを持つオブジェクトは、配列様オブジェクトと呼ばれます。このようなオブジェクトは、他のプロパティやメソッドを持つ場合もありますが、配列の組み込みメソッドは欠けています。

仕様の中を見てみると、ほとんどの組み込みメソッドは、「実際の」配列ではなく、反復可能オブジェクトまたは配列様オブジェクトで動作することを前提としています。なぜなら、それはより抽象的だからです。

Array.from(obj[, mapFn, thisArg])は、反復可能オブジェクトまたは配列様オブジェクトobjから実際のArrayを作成し、その後、その配列メソッドを使用できます。オプションの引数mapFnthisArgを使用すると、各アイテムに関数を適用できます。

チュートリアルマップ

コメント

コメントする前にこれを読んでください…
  • 改善すべき点に関するご提案がありましたら、コメントする代わりにGitHub issueまたはプルリクエストを送信してください。
  • 記事の内容が理解できない場合は、詳しく説明してください。
  • いくつかのコードを挿入するには<code>タグを使用し、いくつかの行の場合は<pre>タグで囲み、10行以上の場合にはサンドボックス(plnkrjsbincodepen…)を使用してください。