Q1invoices.filter((invoice) => invoice.id === "B-302") が返すのはどれですか?
文法まとめ — 配列メソッド・非同期・例外の早見表
配列メソッドを戻り値の形で、非同期処理をコールバック方式・then・await の 3 つの書き方で、例外を失敗が届く場所で見比べる早見表です。チェーンを続けられる条件もまとめます。
請求の一覧から合計を出す、アップロードの完了を待つ、失敗を受け取って案内を出すといった処理には、同じ目的に使える書き方が複数あります。選び方を誤ると、配列のつもりで undefined を受け取ったり、失敗がどの catch にも届かなかったりします。
この記事では、配列メソッド・非同期の待ち方・例外の受け取り方の使い分けを扱います。
欲しい結果の形から選ぶ — 配列メソッドの戻り値
請求の一覧から、支払い済みの件数、最初の未払いの 1 件、請求額の合計をそれぞれ出したいとします。配列メソッドはどれもコールバックを渡す同じ形で呼ぶので、見た目だけでは、配列が返るのか値が 1 つ返るのかを区別できません。
ここでは、メソッドが返す値を 戻り値の形(新しい配列・配列の中にあった要素そのもの・真偽値や合計のように配列から計算した値のどれか)で分けます。形が決まれば、返った値に続けて join を呼べるのか、.id のようにプロパティを読むのかも決まります。
const invoices = [
{ id: "B-301", amount: 4800, paid: true },
{ id: "B-302", amount: 1200, paid: false },
{ id: "B-303", amount: 3600, paid: true },
{ id: "B-304", amount: 2500, paid: false },
];
// 配列が返る: map は件数そのまま、filter は条件に合う分だけ
console.log(invoices.map((invoice) => invoice.id).join(", ")); // B-301, B-302, B-303, B-304
console.log(invoices.filter((invoice) => invoice.paid).length); // 2
// 要素そのものが返る: 最初に条件に合った 1 件
console.log(invoices.find((invoice) => !invoice.paid).id); // B-302
// 配列から計算した値が返る: some は真偽値、reduce は畳み込んだ値
console.log(invoices.some((invoice) => invoice.amount >= 4000)); // true
console.log(invoices.reduce((total, invoice) => total + invoice.amount, 0)); // 12100
1 件だけ欲しいときに filter を使うと、返るのは 1 件入りの配列なので、.id を読む前に [0] を付けることになります。下の表は、このカテゴリで扱った、関数を渡す配列メソッドを戻り値の形で並べたものです。
| メソッド | 返すもの | 注意 |
|---|---|---|
| map | 新しい配列(件数は同じ) | 元の配列は変わらない |
| filter | 新しい配列(合う分だけ) | 0 件なら空の配列 |
| find / findIndex | 要素 / 位置 | 無いと undefined / -1 |
| some / every | 真偽値 | 空の配列では false / true |
| reduce | 最後の周が返した値 | 空の配列は初期値が必要 |
| sort / toSorted | 並べ替えた配列 | sort は元の配列も変わる |
続けて呼べるかを見分ける — チェーンと forEach
未払いの請求額だけを合計したいとします。filter の結果を変数に入れ、その変数で map を呼び、さらに reduce を呼ぶと、あとで使わない途中の配列にも名前が増えます。1 つの式につなげたくても、どのメソッドのうしろなら続けられるかを知らないと、つないだ位置で TypeError になります。
チェーン(前のメソッドの戻り値に対して次のメソッドを呼ぶ書き方)では、.filter(...) のような 1 回の呼び出しを 1 段と数え、各段は 1 つ前の段が返した値に対して呼ばれます。配列メソッドを続けられるのは、前の段が配列を返したときだけです。
// invoices は前の章と同じ 4 件の請求
// 配列が返る間は、うしろに配列メソッドを続けられる
const unpaidTotal = invoices
.filter((invoice) => !invoice.paid) // 配列: B-302 と B-304 の 2 件
.map((invoice) => invoice.amount) // 配列: [1200, 2500]
.reduce((total, amount) => total + amount, 0); // 数値: 3700
console.log(unpaidTotal); // 3700
// toSorted も配列を返すので、map と join まで続けられる
const idsByAmount = invoices.toSorted((a, b) => b.amount - a.amount).map((invoice) => invoice.id);
console.log(idsByAmount.join(", ")); // B-301, B-303, B-304, B-302
// forEach が返すのは undefined なので、うしろに続けると止まる
invoices.forEach((invoice) => console.log(invoice.id)).map((invoice) => invoice.id);
// B-301 から B-304 までを表示したあと
// TypeError: Cannot read properties of undefined (reading 'map')
配列を返さない段は、チェーンの最後に置きます。find のうしろでは .id のようにプロパティを読めても map は呼べず、reduce が返した数値はそのまま変数に入れて使います。
find の後ろで map を呼ぶと文言が変わる
find が返した要素や reduce が返した数値に .map(...) を続けると、TypeError: invoices.find(...).map is not a function のような TypeError で止まります。forEach の後ろとは文言が違いますが、どちらも .map の 1 つ前の段が配列を返していません。
待ち方を比べる — コールバック / then / await
写真をアップロードしてから、その縮小版を作るとします。どちらも結果が届くまで時間がかかるため、関数の戻り値として結果を返せず、2 つ目の手順は 1 つ目の結果が届いてから始める必要があります。
下のコードの ① は コールバック方式(処理を始める関数に、終わったら呼ぶ関数を引数で渡す書き方。Promise は返さない)で、setTimeout もこの形です。② は受け取った Promise の then に関数を渡す書き方、③ は async 関数の中で await を使う書き方です。
// ① コールバック方式: 終わったら呼ぶ関数を渡し、次の手順はその中に書く
// later は 100ms 後に value を渡して callback を呼ぶ(Promise は返さない)
function later(value, callback) {
setTimeout(() => callback(value), 100);
}
later("photo-17.jpg", (fileName) => {
later(`${fileName} の縮小版`, (thumbnail) => {
console.log(`① ${thumbnail}を作りました`); // ① photo-17.jpg の縮小版を作りました
});
});
// ② then: 次の手順を .then でつなぐ(delay は Promise の記事と同じ)
delay(100, "photo-17.jpg")
.then((fileName) => delay(100, `${fileName} の縮小版`))
.then((thumbnail) => console.log(`② ${thumbnail}を作りました`)); // ② photo-17.jpg の縮小版を作りました
// ③ await: 次の手順を下の行に書き、結果は変数で受け取る(前の記事と同じく async 関数の中で動く前提)
const fileName = await delay(100, "photo-17.jpg");
const thumbnail = await delay(100, `${fileName} の縮小版`);
console.log(`③ ${thumbnail}を作りました`); // ③ photo-17.jpg の縮小版を作りました
later("photo-17.jpg", (fileName) => { … })を呼ぶ
- 1 つ目の結果が
fileNameに届く - 2 つ目の
laterをここで呼ぶ
- 2 つ目の結果が
thumbnailに届く - 表示はいちばん内側に書く
delay(100, "photo-17.jpg")から始める
fileNameを受け取り、次のdelayを返す
thumbnailを受け取って表示する
const fileName = await delay(…)const thumbnail = await delay(…)console.log(…)を次の行に書く
2 つ目の then の関数には thumbnail しか届かないので、そこで fileName も使うには外の変数に移すことになります。await なら、変数に入った fileName を下のどの行からでも読めます。下の表は、3 つの書き方を並べたものです。
失敗が届く先を確かめる — try / catch と .catch
在庫を確保する処理が、注文数が在庫を超えたときに失敗するとします。Promise を返さない普通の関数なら、呼んだ行を try / catch で囲めば受け取れますが、Promise を返す関数の失敗は、書き方によって catch 文に届いたり届かなかったりします。
try に続く catch 文は、囲んだ行で起きた例外を受け取ります。Promise の .catch は、その Promise が rejected になったときに渡した関数を呼びます。
function checkStock(count) {
if (count > 3) {
throw new Error(`在庫が足りません: ${count} 点`);
}
}
async function reserveStock(count) {
await delay(100); // delay は Promise の記事と同じ
checkStock(count); // ここで throw すると、返す Promise は rejected になる
}
// ① Promise を返さない関数: 呼んだ行を try で囲む
try { checkStock(5); } catch (error) { console.log(`① ${error.message}`); }
// ② then の書き方: .catch に受け取る関数を渡す
reserveStock(5).catch((error) => console.log(`② ${error.message}`));
// ③ await の書き方: await した行を try で囲む
try { await reserveStock(5); } catch (error) { console.log(`③ ${error.message}`); }
// ① 在庫が足りません: 5 点
// ② 在庫が足りません: 5 点(100ms 後)
// ③ 在庫が足りません: 5 点(100ms 後)
.catch と catch 文のどちらで受け取っても、届くのは同じ Error なので、error.message や instanceof の判定はそのまま使えます。下の表は、この講座で扱った例外の書き方をまとめたものです。
| 書くもの | 役割 | 注意 |
|---|---|---|
| try / catch | 囲んだ行の例外を受け取る | 空の catch は失敗を消す |
| finally | 成否に関係なく最後に動く | 後片付けをまとめる |
| throw new Error | その場で例外を起こす | new Error だけでは止まらない |
| class extends Error | instanceof で見分ける種類 | 作った種類から先に判定 |
| Error の cause | 元の例外を添える | error.cause で読む |
| Promise の .catch | rejected のときに呼ばれる | then の末尾に付ける |
理解度チェック
まずは1問ずつ答えてみましょう。
Q2invoices.filter(...).map(...).reduce(...) で、reduce が畳み込む配列は?
Q3コールバック方式で非同期の手順を 3 つ続けるとき、3 つ目の手順はどこに書きますか?