反復処理プロトコル Iteration protocols

《 初回公開:2023/10/19 , 最終更新:未 》

【 目次 】

for...of 文スプレッド構文を利用して反復処理をおこなうには、処理対象のオブジェクトが反復可能なオブジェクトである必要がある。

(反復可能でない)ただのオブジェクトは反復処理をおこなう事ができない。

let obj =new Object();
// エラー Uncaught TypeError: obj is not iterable
for (const value of obj) {
  // 何かの繰り返し処理
}

ユーザー定義のオブジェクトも反復可能オブジェクトとして実装する事が可能で、そのためには反復可能プロトコルを実装する必要がある。
そして、反復可能オブジェクトを実装するためには反復処理プロトコルを理解する必要がある。

プロトコルとは約束事の事、ChatGPTいわく

プロトコルとは、異なるシステム間で通信するための取り決めや規約のことを指します。通常、異なるシステム間で情報をやり取りする場合、それぞれのシステムが互いに理解できる共通のプロトコルが必要となります。
プロトコルは、データの形式、通信手順、エラー処理などを含み、相互作用の詳細な規約を定めます。
プロトコルには、インターネットプロトコルのような広く知られたものから、特定の用途に限定されたものまで様々な種類があります。
プログラミングにおいては、プロトコルは通常、異なるオブジェクト間でのデータ交換や動作の制御に使用されます。
例えば、反復可能プロトコルやイテレータプロトコルは、JavaScriptの配列やマップ、セットなどのオブジェクトを反復処理するために使用されます。

反復処理プロトコルとは反復処理をおこなうための約束事の事。

反復可能(iterable)プロトコルと反復子(iterator)プロトコル

反復処理プロトコルには、反復子(イテレータ)プロトコル反復可能(イテラブル)プロトコルの2つのプロトコルが存在する。
反復処理プロトコルを理解するにはこの反復子プロトコルと反復可能プロトコルの違いを認識する事が重要だと。

反復子プロトコルを実装したオブジェクトをイテレータ(オブジェクト)と呼ぶ。
それに対して、反復可能プロトコルを実装したオブジェクトをイテラブル(オブジェクト)と呼ぶ。

for...of 文やスプレッド構文を使うには、反復子プロトコルではなく反復可能プロトコルを実装したオブジェクトが必要。
でも、反復子プロトコルと反復可能プロトコルとは密接な関係があって、反復子プロトコルは単独で利用可能であるが、反復可能プロトコルを実装するためには反復子プロトコルを必ず利用する必要がある。

ChatGPTいわく

反復処理プロトコル(Iteration Protocol)は、JavaScriptでオブジェクトを反復処理するための仕組みです。このプロトコルに従うオブジェクトは、 Symbol.iterator メソッドを実装している必要があります。

Symbol.iterator メソッドは、オブジェクトの反復処理に使用されるイテレーター(Iterator)を返します。イテレーターは、 next() メソッドを持ち、それを呼び出すことで反復処理が進行します。 next() メソッドは、反復処理の進行に応じて、 value と done という2つのプロパティを持ったオブジェクトを返します。 value プロパティには、次の値が含まれます。 done プロパティには、反復処理が完了したかどうかを示すブール値が含まれます。

具体的には、 Array や Map 、 Set などの組み込みオブジェクトは、反復処理プロトコルに準拠しており、 Symbol.iterator メソッドを持っています。また、オブジェクトリテラルには @@iterator という内部メソッドがあり、これを定義することでオブジェクトリテラルも反復処理可能になります。

反復処理プロトコルを使用することで、オブジェクトの反復処理が一貫した方法で行えるため、コードの再利用性や可読性が向上することが期待できます。

反復子プロトコルとイテレーター(オブジェクト)

反復子プロトコルを満たすべき条件とは

イテレーターは反復子プロトコルを実装するオブジェクトを指す。

反復子プロトコルの満たすべき条件とは
nextメソッドを持つこと、そしてそのnextメソッドの戻り値ははIteratorResult インタフェースに適合したオブジェクトを返さなければならない。
IteratorResultインタフェースに適合したオブジェクトというのはオブジェクトの要素(プロパティ)として
valueとdoneという要素を含んでいる事。
valueプロパティは繰り返しの結果,得られる値であり、doneプロパティはtrueかfalseの値を持つ。
doneプロパティの値がtrueの場合はまだ繰り返しの途中であり、falseの場合は繰り返しの最後に達した事を示す。
doneがtrueの場合は、valueプロパティは必要ない。
また、次に処理するべき値がない場合にはdoneプロパティさえ必要ない。

イテレータの実装

具体的にイテレータのコードを実装してみる、
以下のコードは10から20までの数値を2つおきに返すイテレータのコードである。

// 1回だけ利用可能なイテレータ
const stepIterator = {
  min: 10,
  max: 20,
  step: 2,
  current: undefined,
  next: function () {
    if (!this.current) {
      this.current = this.min;
    } else {
      this.current += this.step;
    }
    if (this.current <= this.max) {
      return { value: this.current, done: false };
    } else {
      return { done: true };
    }
  }
}

このイテレータオブジェクトを使って、イテレータのnextメソッドの返すオブジェクトのvalueプロパティの値をコンソール出力すると。

let result = stepIterator.next();
while (!result.done) {
  console.log(result.value);  // 10 12 14 16 18 20
  result = stepIterator.next();
}

当然の事ながらこのイテレータstepIteratorはいったん終わりまで達してしまうとこれで終わりで、再利用できない。

result = stepIterator.next();
console.log(JSON.stringify(result));    // {"done":true}

doneプロパティの値はtrueの場合、valueプロパティは必要無い。

今度は、このイテレータオブジェクトの機能をクラスを使って実装してみる。

class StepIteratorCls {
  constructor(min, max, step) {
    this.min = min;
    this.max = max;
    this.step = step;
    this.current = undefined;
  }
  next() {
    if (!this.current) {
      this.current = this.min;
    } else {
      this.current += this.step;
    }
    if (this.current <= this.max) {
      return { value: this.current, done: false };
    } else {
      return { done: true };
    }
  }
}

クラスを使う事で新しいStepIteratorのインスタンスを再生成すれは何度でもイテレータの再利用が可能。

let stepIterator = new StepIteratorCls(10, 20, 2)
let result = stepIterator.next();
while (!result.done) {
  console.log(result.value);    // 10 12 14 16 18 20
  result = stepIterator.next();
}
stepIterator = new StepIteratorCls(10, 20, 2)
result = stepIterator.next();
while (!result.done) {
  console.log(result.value);    // 10 12 14 16 18 20
  result = stepIterator.next();
}

これでもいいのだが、次のイテラブルオブジェクトへの伏線としてイテレータを返す関数としてこの機能実装してみる。

function generateStepIterator(min, max, step) {
  let current = undefined;
  return {
    next: function () {
      if (!current) {
        current = min;
      } else {
        current += step;
      }
      if (current <= max) {
        return { value: current, done: false };
      } else {
        return { done: true };
      }
    }
  }
}

イテレータを返す関数として実装する事で、関数からイテレータを何度でも再生成して利用する事ができる。

function generateStepIterator(min, max, step) {
  let current = undefined;
  return {
    next: function () {
      if (!current) {
        current = min;
      } else {
        current += step;
      }
      if (current <= max) {
        return { value: current, done: false };
      } else {
        return { done: true };
      }
    }
  };
}

しかし、イテレーターを実装しただけではイテラブルにはならないので配列のようにfor...of 文を使って値を取り出す事はできない。
次のコードはエラーになる。

stepIterator = generateStepIterator(10, 20, 2)
// エラー Uncaught TypeError: stepIterator is not iterable
for (let value of stepIterator) {
  console.log(value); // 1 3 5 7 9
}

for...of 文を使って値を取り出すには次の反復可能プロトコルを実装する必要がある。

反復可能プロトコルと反復可能(イテラブル)オブジェクト

javascript 反復可能オブジェクト

for...ofループやスプレッド構文を利用するためには、オブジェクトが反復可能である必要がある。
反復可能プロトコルを実装する事でオブジェクトは反復可能になる。
反復可能なオブジェクトはイテラブルオブジェクトとも呼ばれる。

反復可能なオブジェクトを実装するにはイテレータが必要。
なので、イテレータ単独で使用することはあまり利用価値がなく、反復可能オブジェクトとセットで使われる事になる。

反復可能プロトコルを満たすべき条件とは

反復可能オブジェクト(イテラブルオブジェクト)は反復可能プロトコル(イテラブルプロトコル)を実装するオブジェクトを指す。

反復可能プロトコルの満たすべき条件とは
定義済みのシンボルSymbol.iteratorという名前のメソッドを持ち、そのメソッドの戻り値はイテレータを返さなければならない事。
Symbol.iteratorメソッドには引数を持つ事はできない。
あるいは引数があったとしても引数には何も渡されない(undefinedに)。

Symbol.iteratorメソッドは、@@iteratorメソッドとも呼ばれる。
但し、コード上で@@iteratorと記述する事はできない。
あくまでも、Symbol.iteratorの事を@@iteratorとして呼んでいるだけの事。

反復可能オブジェクトを継承したオブジェクトもプロトタイプチェーンをたどる事で反復可能なオブジェクトになる。

反復可能オブジェクト(イテラブル)の実装

既存のイテレータのコードを利用してイテラブルなオブジェトを実装するのは簡単で、@@iteratorメソッドを追加してその戻り値がthisを返すようにするだけでいい。
イテレータの実装の項で示した「1回だけ利用可能なイテレータ」のコードを改良してイテラブルなオブジェトを実装すると。

stepIteratorを強引に再利用可能に

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
const stepIterator = {
  min: 10,
  max: 20,
  step: 2,
  current: undefined,
  next: function () {
    if (!this.current) {
      this.current = this.min;
    } else {
      this.current += this.step;
    }
    if (this.current <= this.max) {
      return { value: this.current, done: false }
    } else {
      this.current = undefined;
      return { done: true };
    }
  },
  [Symbol.iterator]: function () { return this; }
}

ハイライト表示の行が変更した部分で、19行目のコードがイテレータをイテラブルにするために追加した部分。
また、これだけでは一度しか反復することができないので、15行目のコードを追加してイテレータをリセットしてやる事で何度も繰り返し可能な反復可能オブジェクトに。

for (let value of stepIterator) {
  console.log(value);   // 10 12 14 16 18 20
}
// 15行目のコードを追加する事で再利用可能に
for (let value of stepIterator) {
  console.log(value);   // 10 12 14 16 18 20
}

クラスの例も。
StepIteratorClsをイテラブルに修正。

class StepIteratorCls {
  constructor(min, max, step) {
    this.min = min;
    this.max = max;
    this.step = step;
    this.current = undefined;
  }
  next() {
    if (!this.current) {
      this.current = this.min;
    } else {
      this.current += this.step;
    }
    if (this.current <= this.max) {
      return { value: this.current, done: false };
    } else {
      return { done: true };
    }
  }
  [Symbol.iterator]() { return this; }
}
for (let value of new StepIteratorCls(10, 20, 2)) {
  console.log(value);   // 10 12 14 16 18 20
}
// 新しいインスタンスを生成する事により再利用は可能に
for (let value of new StepIteratorCls(10, 20, 2)) {
  console.log(value);   // 10 12 14 16 18 20
}

反復可能オブジェクトを継承したオブジェクトもプロトタイプチェーンをたどる事で反復可能なオブジェクトになるので、
イテラブルなクラスを継承したクラスのインスタンスも反復可能に。

class StepIteratorEx extends StepIteratorCls {
  constructor(min, max, step) {
    super(min, max, step)
  }
}

for (let value of new StepIteratorEx(10, 20, 2)) {
  console.log(value);   // 10 12 14 16 18 20
}

イテレータを返す関数をイテラブルにするには、
シンボルSymbol.iteratorの値としてイテレーターを返す関数を指定する。
前述のgenerateStepIterator関数からイテラブルなオブジェクトを作成すると。
Symbol.iterator関数は引数を持たないのでこれを修正して。

let iterableObj = {
  [Symbol.iterator]: function () {
    let min = 10, max = 20, step = 2;
    let current = undefined;
    return {
      next: function () {
        if (!current) {
          current = min;
        } else {
          current += step;
        }
        if (current <= max) {
          return { value: current, done: false };
        } else {
          return { done: true };
        }
      }
    }
  }
}
for (let value of iterableObj) {
  console.log(value);   // 10 12 14 16 18 20
}

これまで、イテレータとイテラブルにについていろいろ述べてきたが、実は次のジェネレータを利用するともっと簡単にイテレータとイテラブルを実装する事ができる。

ページのトップへ戻る