Q1reduce に渡すコールバックの 1 つ目の引数に入るのは何ですか?
reduce — 配列を 1 つの値に畳み込む
配列を 1 つの値にまとめる reduce を扱います。アキュムレータと初期値の受け渡し、合計・最大・カテゴリ別の集計、空の配列で起きる TypeError まで確かめます。
注文の明細から合計金額を出したいとき、map は件数が変わらず、filter は要素が減るだけで、どちらも返るのは配列です。合計や最大値のように配列を 1 つの値にまとめる処理は、この 2 つのメソッドでは書けません。
この記事では、配列を 1 つの値にまとめる reduce と、その起点になる初期値を扱います。
配列を 1 つの値にまとめる — reduce と初期値
買い物カートの合計金額を画面に出したいとします。forEach で書くと、合計を入れる変数がループの外に残り、あとの行からも書き換えられる状態になります。足し込むたびに値が変わるので、その変数は const ではなく let で宣言することになります。
配列のメソッド reduce(要素を 1 件ずつコールバックに渡し、最後の戻り値だけを返すメソッド)のコールバックは、map や filter と違い、1 つ目の引数が アキュムレータ(前の周の戻り値が入る引数)で、要素は 2 つ目です。reduce 自身の第 2 引数には、初期値(1 周目のアキュムレータに入る値)を書きます。
const cartItems = [
{ name: "ノート PC スタンド", price: 3200 },
{ name: "USB ハブ", price: 2480 },
{ name: "HDMI ケーブル", price: 1180 },
];
// 1 つ目の引数が前の周の戻り値、2 つ目が今の要素
const addPrice = (total, item) => total + item.price;
// 第 2 引数の 0 が 1 周目の total になる
const totalPrice = cartItems.reduce(addPrice, 0);
console.log(totalPrice); // 6860
// 返るのは配列ではなく 1 つの値
console.log(typeof totalPrice); // number
total として渡ります。最後の周が返した 6860 が reduce の結果です。途中経過を持つ変数を自分で用意しなくてよいのは、周と周のあいだで値を渡す役目を reduce が持っているからです。次の周へは数値だけでなく、要素やオブジェクトも渡せます。
大きい方を残す — 数値と要素どちらを持ち回るか
配信した動画のうち、再生数が最も多いものを知りたいとします。合計と違って足し合わせるのではなく、比べて大きい方だけを次の周へ残す形です。ここで決めることになるのが、1 周目のアキュムレータに何を置くかです。
アキュムレータには数値も要素そのものも置けます。第 2 引数に 0 を置けば数値を、videos[0] を置けば要素そのものを次の周へ渡す形になります。初期値には、コールバックが返すものと同じ種類の値を置きます。
const videos = [
{ title: "SQL 入門 #1", views: 8200 },
{ title: "Linux 入門 #3", views: 15400 },
{ title: "Git の基本", views: 9700 },
];
// 初期値が数値なので、比べるのも返すのも数値
const maxViews = videos.reduce((max, video) => Math.max(max, video.views), 0);
console.log(maxViews); // 15400
// 初期値が要素なので、比べるのも返すのも要素
const topVideo = videos.reduce(
(top, video) => (video.views > top.views ? video : top),
videos[0],
);
console.log(topVideo.title); // Linux 入門 #3
どちらの行も比べているのは views の値で、違うのは次の周へ渡すものだけです。下の行で初期値を 0 にすると、1 周目の top.views が undefined なので比較は毎回 false になり、最後まで 0 が返ります。
初期値 0 は負の数を含むと合いません
気温の記録のようにマイナスの値が混ざる配列で初期値に 0 を置くと、実際の最大値が -3 でも 0 のほうが大きいまま残り、配列に無い値が結果になります。要素どうしを比べる形にして初期値を配列の 0 番目にすれば、値の範囲に左右されません。
件数をオブジェクトに畳む — グルーピング
問い合わせがカテゴリごとに何件あるかを知りたいとします。合計のように結果が数値 1 つでは済まず、カテゴリの数だけ値を持つ集計になります。空のオブジェクトを外に置いて forEach で足し込む形でも書けますが、集計用の入れ物が処理の外に残ります。
初期値には {} も置けます。このときのアキュムレータは集計中のオブジェクトで、コールバックはキーを更新したあと そのオブジェクトを返します。まだ登録の無いキーを読むと undefined なので、?? 0 を挟んで 0 から数え始めます。
const tickets = [
{ subject: "返品したい", category: "返品" },
{ subject: "商品が届かない", category: "配送" },
{ subject: "伝票を再発行してほしい", category: "配送" },
];
// カテゴリごとに 1 件ずつ数え上げる
const countByCategory = tickets.reduce((counts, ticket) => {
counts[ticket.category] = (counts[ticket.category] ?? 0) + 1;
return counts; // 更新した counts を次の周へ渡す
}, {}); // この {} が 1 周目の counts になる
console.log(JSON.stringify(countByCategory)); // {"返品":1,"配送":2}
console.log(countByCategory["配送"]); // 2
counts のキーを 1 増やし、そのまま返しています。書き込み先は、初期値に渡した 1 つのオブジェクトだけです。初期値の {} は、集計結果だけを入れるための新しいオブジェクトで、tickets の要素には何も書き込みません。下の表は、ここまでの 3 つの使い方で、初期値とコールバックが返すものの組み合わせをまとめたものです。
| 畳み込みたいもの | 初期値に置くもの | コールバックが返すもの |
|---|---|---|
| 合計 | 0 | 前の周の合計に値を足した数値 |
| 最も大きい要素 | 配列の 0 番目 | 比べて大きいほうの要素 |
| 分類ごとの件数 | 空のオブジェクト | キーを更新したオブジェクト |
要素が 1 つも無いとき — 初期値を省いた reduce
絞り込んだ結果が 0 件になることがあります。当日の出荷が 1 件も無い日に合計を求めるような場面で、reduce は書き方によって 0 を返すこともエラーで止まることもあります。
第 2 引数の初期値は省略できます。省いた場合は配列の 0 番目が最初のアキュムレータになり、コールバックは 1 番目の要素から呼ばれます。空の配列では取り出す 0 番目が無いため、TypeError が投げられます。
const yesterdayAmounts = [3200, 2480, 1180];
const todayAmounts = [];
// 初期値を省くと、0 番目の 3200 が最初の total になる
console.log(yesterdayAmounts.reduce((total, amount) => total + amount)); // 6860
// 空の配列でも、初期値があればそれがそのまま返る
console.log(todayAmounts.reduce((total, amount) => total + amount, 0)); // 0
// 初期値が無いと最初の total を決められない
// todayAmounts.reduce((total, amount) => total + amount);
// TypeError: Reduce of empty array with no initial value
手元のデータで動いていても、条件に合う要素がたまたま 1 件も無い日に初めて止まるため、書いた時点では気づきにくい失敗です。filter で絞った結果を reduce に渡すときは、0 件に備えて初期値を渡します。
{} を省くと先頭の要素に書き込まれる
countByCategory の例で最後の {} を省くと、tickets[0] の問い合わせオブジェクトそのものが最初の counts になります。エラーは出ずに、そのオブジェクトへ 配送 のキーが書き足され、返品 の 1 件は数えられません。要素があっても、オブジェクトに畳むときは初期値を渡します。
理解度チェック
まずは1問ずつ答えてみましょう。
Q2要素どうしの views を比べて大きいほうを返す reduce に、初期値 0 を渡すと何が返りますか?
Q3空の配列に対して、初期値を省いた reduce を呼ぶと何が起きますか?