♻️

[JavaScript] 繰り返しを操る、Iterator

2024/12/03に公開

はじめに

実務でイテレータを触ることがあったので、調べてまとめてみたものです。
間違いがありましたらコメントいただけると嬉しいです🫶

さて、JavaScript を触っているとたまに見ます、Iterator とか Iterable の文字。
for や while のことを「イテレータ」と呼ぶと昔学んだ記憶があるので、なんとなく反復するものだろうという程度の認識で使っていました。しかしよくよく調べるとけっこう便利そうな機能がでてきたので、年度の納めに学んでみようと思った次第です。

イテレータ(Iterator) とは?

https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Iterators_and_generators

基本的には、以下の条件をすべておさえるオブジェクトを指します。

  • next() メソッドが定義されている
  • next() の戻り値は IterableResult であること
    • IterableResult{ value: value, done: done } の形式のオブジェクトだよ
    • value にはイテレータの次の値、done には最後まで値を掘り出し終わったかの真偽値を入れるよ

ぱっとみ難しく見えますね。ここで最もわかりやすい例として配列を扱ってみます。
以下のような配列を用意しました。

const a = ["たぬき", "きつね", "いたち"]

この配列に対して [Symbol.iterator]() メソッドを呼び出すことで、イテレータを生成することができます。

const a = ["たぬき", "きつね", "いたち"]
const iter = a[Symbol.iterator]()   // Object [Array Iterator] {}

配列からイテレータが生成できる理由は後で説明するとして。
このイテレータに対して、試しに next() メソッドを呼び出してみます。正しくイテレータが生成されていれば、前述の条件のように IterableResult が返るはずですね。

const a = ["たぬき", "きつね", "いたち"]
const iter = a[Symbol.iterator]()
console.log(iter.next())

// { value: 'たぬき', done: false }

valuedone を持った IterableResult が返りました。初めて呼び出したので、中身は配列の先頭にあった「たぬき」ですね。どうやら正しくイテレータが作成できていそうです。
続けて、すべての要素を取り出してみます。

const a = ["たぬき", "きつね", "いたち"]
const iter = a[Symbol.iterator]()

while (true) {
    const result = iter.next()
    console.log(result)
    
    // すべて取り出したら止める
    if(result.done) break;   
}

// { value: 'たぬき', done: false }
// { value: 'きつね', done: false }
// { value: 'いたち', done: false }
// { value: undefined, done: true }

配列の最後まで値を取り出した後、donetrue になっているのがわかります。
このように、next() メソッドを実行したときに繰り返しの次の値がいつでも取り出せるのがイテレータです。

イテラブル(Iterable) とは?

同じような単語ですが、イテラブル(Iterable) というのもあります。
こちらは「反復可能」を示すもので、以下の条件を全ておさえるオブジェクトを指します。

  • [Symbol.iterator]() というメソッドを持つ
  • [Symbol.iterator]() を実行すると、イテレータを返す

イテラブルなオブジェクトは for ~ of 構文でループできたり、スプレッド構文で展開できたりします。

イテラブルなオブジェクト

以下に一例を示します。

  • 配列(Array)、マップ(Map)、セット(Set)
    配列に限らず、コレクションのほとんどはイテラブルだと思います。

  • 文字列(String)
    実はイテラブルなオブジェクトです。
    試しに for ~ of 構文に突っ込んでみると、一文字ずつ返してくれます。便利。

for(const s of "吾輩は猫である。名前はたぬき。") {
console.log(s)
}

// 吾
// 輩
// は
// 猫
// で
// ...
  • セグメント(Segment)
    この子を見ることはあまりありませんが、形態素解析に使用する Intl.Segmentersegment 関数で返されます。
    余談ですが、isWordLike に単語であるかどうかを詰めて返してくれるので、句読点などを弾けたりします。便利。
const str = "吾輩は猫である。名前はたぬき。";
const segmenterJa = new Intl.Segmenter("ja-JP", { granularity: "word" });

const segments = segmenterJa.segment(str);

for(const segment of segments[Symbol.iterator]()) {
  console.log(segment);
}

// { segment: '吾輩', index: 0, input: '吾輩は猫である。名前はたぬき。', isWordLike: true }
// { segment: 'は', index: 2, input: '吾輩は猫である。名前はたぬき。', isWordLike: true }
// { segment: '猫', index: 3, input: '吾輩は猫である。名前はたぬき。', isWordLike: true }
// { segment: 'で', index: 4, input: '吾輩は猫である。名前はたぬき。', isWordLike: true }
// { segment: 'ある', index: 5, input: '吾輩は猫である。名前はたぬき。', isWordLike: true }
// { segment: '。', index: 7, input: '吾輩は猫である。名前はたぬき。', isWordLike: false }
// ...

使ってみる

実際にイテレータを活用してみます。

連番を作成する

1, 2, 3, ... のように、連続する値を返すシンプルなイテレータをつくりたいとします。
初めの値(start)と終わりの値(end)、一度のイテレートでどれくらい増えるか(step)を引数にとり、イテレータを返す関数を実装してみます。start は範囲に含み、endは範囲に含みません。

(実装は MDN を参考にしました)

function makeRangeIterator(start = 0, end = Infinity, step = 1) {
  let nextIndex = start;
  let iterationCount = 0;

  const rangeIterator = {
    next: function () {
      let result;
      if (nextIndex < end) {
        result = { value: nextIndex, done: false };
        nextIndex += step;
        iterationCount++;
        return result;
      }
      return { value: iterationCount, done: true };
    },
  };
  return rangeIterator;
}

イテレータの利点は、総要素数を気にしなくていいところです。0 ~ Infinity の要素を持った配列を作成することは(物理性能的に)不可能ですが、イテレータは都度値を生成するため、サイズを気にする必要がなくなりますね。
さて、実際に使用すると以下のような感じになります。

const iter = makeRangeIterator(1, 10)

let result = iter.next();
while (!result.done) {
    result = iter.next()
    console.log(result)
    
    // すべて取り出したら止める
    if(result.done) break;
    
}

// { value: 1, done: false }
// { value: 2, done: false }
// { value: 3, done: false }
// { value: 4, done: false }
// { value: 5, done: false }
// { value: 6, done: false }
// { value: 7, done: false }
// { value: 8, done: false }
// { value: 9, done: false }
// { value: 9, done: true }

問題なく実装できました。

ジェネレータ関数(Generator Function)で実装する

さて、実は JavaScript にはより便利なイテレータを生成するための仕組みがあります。これを ジェネレータ関数(Generator Function) と呼びます。

まず、先ほどのコードを見てみます。

function makeRangeIterator(start = 0, end = Infinity, step = 1) {
  let nextIndex = start;
  let iterationCount = 0;

  const rangeIterator = {
    next: function () {
      let result;
      if (nextIndex < end) {
        result = { value: nextIndex, done: false };
        nextIndex += step;
        iterationCount++;
        return result;
      }
      return { value: iterationCount, done: true };
    },
  };
  return rangeIterator;
}

これを、ジェネレータ関数を使って書き換えると以下のようになります。

(実装は MDN を参考にしています)

function* makeRangeIterator(start = 0, end = 100, step = 1) {   // ①
  let iterationCount = 0;
  for (let i = start; i < end; i += step) {
    iterationCount++;
    yield i;   // ②
  }
}

すっきりしましたね。順に見ていきます。

①:ジェネレータの定義

ジェネレータ関数は function* で定義します。
これがついた関数は、実行されたときに中のコードを実行せず、かわりに ジェネレータ(ジェネレータ関数から生まれたイテレータ) を返します。

②:yield で値を返す

ジェネレータのnext()メソッドを実行すると、はじめてジェネレータ関数の中身が実行されます。
このとき、yield に注目です。ジェネレータは、いちど実行されると yield がある行まで処理を行います。そして、yield で指定された値を返し、一時停止します。次にジェネレータが実行されるとき、最初からではなく、 以前処理を返した yield の次の行から処理をスタートさせます

これがジェネレータ関数、およびジェネレータによるイテレータの全貌です。
余談ですが、ジェネレータ関数内でreturnしたら後続するyieldがあるかどうかに関わらずdonetrueIteratorResultを返しました。

参考

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Iterator
https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Iterators_and_generators
https://qiita.com/kura07/items/cf168a7ea20e8c2554c6
https://qiita.com/kura07/items/d1a57ea64ef5c3de8528

Progate Path コミュニティ

Discussion