順番に読み進めながら学べます

イテレータとジェネレータ — 値を 1 つずつ取り出す

値を 1 つずつ返すイテレータと、yield で止まりながら進むジェネレータを扱います。next() の進み方、for...of とスプレッド構文での取り出し方、終わりの無い列から必要な数だけ取り出す書き方まで確かめます。

整理券の番号を 1 枚ずつ発行したり、長いレビュー一覧を数件ずつ区切って出したりするとき、番号やページを先にすべて作ると、使われない分まで用意することになります。クロージャで次の値を覚える関数も書けますが、どこまで進んだかと終わったかどうかを自分で管理する必要があります。

この記事では、値を 1 つずつ返す イテレータ と、それを関数の形で作れる ジェネレータ を扱います。

続きから再開する関数 — ジェネレータと yield

窓口の呼び出し番号を、呼ばれるたびに 101、102、103 と 1 つずつ返したいとします。ふつうの関数は return した時点で終わり、次に呼ぶと先頭から実行し直すので、どこまで返したかを外側の変数に持たせる必要があります。

ジェネレータ関数function* で宣言し、中で yield を使える関数)を呼ぶと、本体は動かずにジェネレータ(本体を少しずつ進めるオブジェクト)が返ります。その next() を呼ぶと、本体が次の yield まで進んで止まります。戻り値は、yield の値を value、終わったかどうかを done に入れたオブジェクトです。

// function* で宣言すると、ジェネレータ関数になる
function* callNumbers() {
  console.log("受付を始めます");
  yield 101;
  yield 102;
  yield 103;
}

// 呼んだ時点では本体は動かず、ジェネレータが返るだけ
const numbers = callNumbers();

// next() を呼ぶたびに、次の yield まで進んで止まる
const first = numbers.next();                   // ここで「受付を始めます」が表示される
console.log(`${first.value} / ${first.done}`);  // 101 / false
console.log(numbers.next().value);              // 102
console.log(numbers.next().value);              // 103

// 返す yield が残っていないと、done が true になる
const last = numbers.next();
console.log(`${last.value} / ${last.done}`);    // undefined / true
next() ごとに止まる位置が進む
numbers =callNumbers()本体はまだ1 行も動かない1 回目のnext()受付を始めます を表示yield 101 で止まる2・3 回目のnext()yield 102・103で順に止まる4 回目のnext()value は undefineddone は true
「受付を始めます」は 1 回目の next() で表示され、yield 101 で止まります。next() のたびに次の yield まで進みます

本体の最後まで進むか return を実行すると donetrue になり、それより後の next() では本体が動きません。どこまで進んだかはジェネレータ自身が持つので、呼び出し番号を覚える変数を外に置かずに済みます。

yield はコールバックの中には書けない

yield を書けるのは、function* で宣言した関数の波括弧の中だけです。forEach に渡したアロー関数の中は別の関数なので、書くと SyntaxError になり、エラー文に yield の語は出ません。配列の要素を 1 つずつ返すときは、forfor...of の本体に yield を書きます。

整理券を用意した枚数だけ発行します。ticketCount は宣言済みです。

① issueTickets をジェネレータ関数にし、1 から count までを返してください。

② 返し終えたら「受付を終了しました」を return してください。

③ ticketCount を渡してジェネレータを受け取ってください。

④ 4 回取り出し、value と done を「1 / false」の形で表示してください。

(正しく実行できれば解説が表示されます)

JavaScript / TypeScript エディタ

コードを実行してください

next() を書かずに回す — イテレータプロトコル

配送状況を「受付」「出荷」「配達中」の順に表示したいとします。next() を 1 回ずつ書くと段階の数だけ行が増え、段階が増えるたびに書き足すことになります。while で回す場合も、done の判定と次の next() を自分で書く必要があります。

next() を呼ぶたびに valuedone を持つオブジェクトを返す、という決まりを イテレータプロトコル と呼び、この決まりに沿うオブジェクトが イテレータ です。1 つ前の章のジェネレータもその 1 つで、for...ofdonetrue になるまで next() を呼んで value を変数に入れます。

function* deliverySteps() {
  yield "受付";
  yield "出荷";
  yield "配達中";
}

// for...of は、done が true になるまで next() を呼ぶ
for (const step of deliverySteps()) {
  console.log(step);            // 受付 / 出荷 / 配達中 の 3 行
}

// 同じ動きを while と next() で書いた形
const steps = deliverySteps();
let result = steps.next();
while (!result.done) {
  console.log(result.value);    // 受付 / 出荷 / 配達中 の 3 行
  result = steps.next();
}
for...of が代わりに行うこと
for...of で回すnext() を自動で呼ぶdone を自動で判定value を step に入れて本体へwhile で自分で回すresult =steps.next()!result.doneを自分で判定result.valueを本体で使う
どちらも done が true になるまで 2〜4 列目を繰り返します。for...of なら next() と done の判定を書かずに済みます

for...of に渡せるのは、配列・Map・ジェネレータのような 反復可能for...of がイテレータを取り出せる)な値です。for...of は値の種類を見分けず、取り出したイテレータの next() を呼ぶだけなので、どれも同じ書き方で回せます。下の表は、変数に入る値をまとめたものです。

for...of に渡すもの1 周ごとに変数に入る値補足
配列 ["受付", "出荷"]要素("受付" → "出荷")回すたびに新しいイテレータが作られ、先頭から取り出す
Map[キー, 値] の 2 要素の配列登録した順に取り出す
ジェネレータyield に書いた値自分自身がイテレータなので、next() で進めた位置が残る
{ name: "ペン" } のようなオブジェクト入らない(TypeError: … is not iterable)反復可能ではないため、キーをたどるなら for...in や Object.entries を使う

商品レビューを 2 件ずつのページに分けて表示します。reviews は宣言済みです。

① paginate で、1 ページ分の配列を先頭から順に返してください。

② reviews を 2 件ずつに分けたページを、繰り返しで 1 つずつ取り出してください。

③ ページごとに「1 ページ目: 」に続けて、投稿者名を「, 」でつないで表示してください。

④ 最後に、ページの数を「全 3 ページ」の形で表示してください。

JavaScript / TypeScript エディタ

コードを実行してください

まとめて配列に変える — スプレッド構文と使い切り

集荷の受付時間を「10:00 / 13:00 / 16:00」のように 1 行にまとめて表示したいとします。ジェネレータは配列ではないので joinlength を持たず、そのままでは件数も数えられません。for...of で 1 件ずつ配列に push すると、そのための行が増えます。

配列の角括弧の中でジェネレータにスプレッド構文の ... を付けると、done が true になるまで値を取り出し、順に並べた新しい配列ができます。const slotList = [...slots]; と書けば、そのあとは joinlength も使えます。

function* pickupSlots() {
  yield "10:00";
  yield "13:00";
  yield "16:00";
}

// ジェネレータは配列ではないので、length を持たない
const slots = pickupSlots();
console.log(slots.length);                // undefined

// [] の中でスプレッド構文を使うと、取り出した値が順に並ぶ
const slotList = [...slots];
console.log(slotList.join(" / "));        // 10:00 / 13:00 / 16:00

// 読み切ったジェネレータをもう一度展開すると空になる
console.log([...slots].length);           // 0

// 呼び直せば、最初から取り出せる別のジェネレータになる
console.log([...pickupSlots()].length);   // 3
2 回目の展開は空の配列になる
1 回目の[...slots]10:00〜16:00 の3 つが並ぶ2 回目の[...slots]空の配列length は 0呼び直した[...pickupSlots()]先頭から読み直すlength は 3
2 回目の [...slots] は、1 回目で最後まで進んだ位置から読み始めます。先頭から取り出せるのは、呼び直したジェネレータだけです

同じ値を 2 か所以上で使うときは、1 度展開した配列を変数に入れ、その配列のほうを使い回します。for...of で最後まで回したジェネレータも、そのあとスプレッド構文で展開すると空の配列です。

会員登録の申し込みから承認済みの会員番号を一覧にします。applications は宣言済みです。

① 承認済みの申し込みだけを選び、会員番号を 1 つずつ返すジェネレータ関数を作ってください。

② ジェネレータを受け取り、1 件目だけを取り出して「先頭: M-4102」の形で表示してください。

③ 残りを配列にまとめ、「残り 2 件: M-4104, M-4105」の形で表示してください。

④ 1 件目も含む全件を「, 」でつないで表示してください。

JavaScript / TypeScript エディタ

コードを実行してください

必要な分だけ作る — 遅延評価と終わりの無い列

お知らせに 1 から順に通し番号を振るとき、何件になるかは先に決まっていません。配列で先に 1000 件分を作ると、3 件しか使わなくても残りの番号まで計算します。件数の上限を決めなければ、配列はそもそも作り終わりません。

遅延評価(値が必要になった時点で初めて計算する進め方)は、ジェネレータで書けます。while (true) で終わりなく yield する関数でも、本体は next() が呼ばれた分しか進みません。配列の分割代入はジェネレータにも使え、左辺に並べた個数だけ値を取り出します。

function* serialFrom(start) {
  let number = start;
  while (true) {
    console.log(`${number} を作りました`);
    yield number;
    number++;
  }
}

// 分割代入は、左辺に並べた個数だけ取り出す
const [first, second] = serialFrom(1);   // 1 を作りました / 2 を作りました
console.log(`${first}, ${second}`);       // 1, 2

// for...of は break で抜けた時点で取り出しをやめる
for (const number of serialFrom(1)) {
  console.log(`お知らせ No.${number}`);
  if (number === 3) break;
}
// 1 を作りました / お知らせ No.1 / 2 を作りました / お知らせ No.2
// 3 を作りました / お知らせ No.3(4 は作られない)
取り出し方で next() の回数が決まる
serialFrom(1)終わりの無い連番const [a, b] =で受け取るfor...of で回し3 を表示して break[...serialFrom(1)]で配列にするnext() は2 回next() は3 回done が true にならない1 と 2 だけ作って終わる1〜3 を作ってループを抜けるnext() を呼び続け処理が終わらない
左の 2 列は取り出す数が決まっているので止まります。スプレッド構文は done が true になるまで読むため、終わりの無い列には使えません

終わりの無い列を配列にするときは、決めた数だけ取り出して return する別のジェネレータを間に挟みます。挟んだほうが終われば donetrue になるので、スプレッド構文もそこで止まります。

yield に届かないと next() が戻らない

while (true) の中で、if の条件を満たしたときだけ yield する書き方では、条件が一度も成り立たないと next() がいつまでも戻りません。実行が終わらず、その下の行も動きません。終わりなく繰り返すときは、条件を満たす値がいずれ必ず来るかを確かめてから書きます。

通信に失敗したときの再試行の待ち時間を、2 倍ずつ延ばして決めます。retryDelays は宣言済みで、値を終わりなく返します。

① take で、source から count 個だけ返して終わるようにしてください。

② 100 から始めて先頭 4 つを配列にし、「 → 」でつないで表示してください。

③ 先頭 5 つを繰り返しで取り出し、「合計: 3100ms」の形で表示してください。

④ 個数に 0 を渡して配列にし、長さを表示してください。

JavaScript / TypeScript エディタ

コードを実行してください
QUIZ

理解度チェック

まずは1問ずつ答えてみましょう。

Q1function* で宣言した関数を呼んで、ジェネレータを受け取った直後、本体はどこまで実行されていますか?

Q23 つの値を yield するジェネレータ slots[...slots] を 2 回続けると、2 回目の長さは?

Q3while (true) で終わりなく yield するジェネレータ gen で、処理が終わらない書き方は?