Q1function* で宣言した関数を呼んで、ジェネレータを受け取った直後、本体はどこまで実行されていますか?
イテレータとジェネレータ — 値を 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
本体の最後まで進むか return を実行すると done が true になり、それより後の next() では本体が動きません。どこまで進んだかはジェネレータ自身が持つので、呼び出し番号を覚える変数を外に置かずに済みます。
yield はコールバックの中には書けない
yield を書けるのは、function* で宣言した関数の波括弧の中だけです。forEach に渡したアロー関数の中は別の関数なので、書くと SyntaxError になり、エラー文に yield の語は出ません。配列の要素を 1 つずつ返すときは、for や for...of の本体に yield を書きます。
next() を書かずに回す — イテレータプロトコル
配送状況を「受付」「出荷」「配達中」の順に表示したいとします。next() を 1 回ずつ書くと段階の数だけ行が増え、段階が増えるたびに書き足すことになります。while で回す場合も、done の判定と次の next() を自分で書く必要があります。
next() を呼ぶたびに value と done を持つオブジェクトを返す、という決まりを イテレータプロトコル と呼び、この決まりに沿うオブジェクトが イテレータ です。1 つ前の章のジェネレータもその 1 つで、for...of は done が true になるまで 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 に渡せるのは、配列・Map・ジェネレータのような 反復可能(for...of がイテレータを取り出せる)な値です。for...of は値の種類を見分けず、取り出したイテレータの next() を呼ぶだけなので、どれも同じ書き方で回せます。下の表は、変数に入る値をまとめたものです。
| for...of に渡すもの | 1 周ごとに変数に入る値 | 補足 |
|---|---|---|
| 配列 ["受付", "出荷"] | 要素("受付" → "出荷") | 回すたびに新しいイテレータが作られ、先頭から取り出す |
| Map | [キー, 値] の 2 要素の配列 | 登録した順に取り出す |
| ジェネレータ | yield に書いた値 | 自分自身がイテレータなので、next() で進めた位置が残る |
| { name: "ペン" } のようなオブジェクト | 入らない(TypeError: … is not iterable) | 反復可能ではないため、キーをたどるなら for...in や Object.entries を使う |
まとめて配列に変える — スプレッド構文と使い切り
集荷の受付時間を「10:00 / 13:00 / 16:00」のように 1 行にまとめて表示したいとします。ジェネレータは配列ではないので join や length を持たず、そのままでは件数も数えられません。for...of で 1 件ずつ配列に push すると、そのための行が増えます。
配列の角括弧の中でジェネレータにスプレッド構文の ... を付けると、done が true になるまで値を取り出し、順に並べた新しい配列ができます。const slotList = [...slots]; と書けば、そのあとは join も length も使えます。
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 度展開した配列を変数に入れ、その配列のほうを使い回します。for...of で最後まで回したジェネレータも、そのあとスプレッド構文で展開すると空の配列です。
必要な分だけ作る — 遅延評価と終わりの無い列
お知らせに 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 は作られない)
終わりの無い列を配列にするときは、決めた数だけ取り出して return する別のジェネレータを間に挟みます。挟んだほうが終われば done が true になるので、スプレッド構文もそこで止まります。
yield に届かないと next() が戻らない
while (true) の中で、if の条件を満たしたときだけ yield する書き方では、条件が一度も成り立たないと next() がいつまでも戻りません。実行が終わらず、その下の行も動きません。終わりなく繰り返すときは、条件を満たす値がいずれ必ず来るかを確かめてから書きます。
理解度チェック
まずは1問ずつ答えてみましょう。
Q23 つの値を yield するジェネレータ slots で [...slots] を 2 回続けると、2 回目の長さは?
Q3while (true) で終わりなく yield するジェネレータ gen で、処理が終わらない書き方は?