つくる 作品データを構造化する
このレッスンで分かること
- 作品データをオブジェクトの配列として設計できます
- 配列をまわしながら件数と合計と最大値を同時に集められます
- 空配列やプロパティ欠落といった「そろっていないデータ」に耐える関数を書けます
つくる 作品データを構造化する とは
第 7 章の総仕上げです。作品オブジェクトの配列を受け取り、件数と合計閲覧数と最新作をまとめた集計結果を返す関数を作ります。
ここまでで何ができるようになったか
第 7 章を通して、山田 太郎さんのプロフィールサイトのデータは次のように育ちました。
// 第 5 章の姿
const works = ["プロフィールサイト", "自己紹介カード", "スキル一覧の表"];
// 第 7 章の姿
const works = [
{ title: "プロフィールサイト", year: 2024, views: 120, published: true },
{ title: "自己紹介カード", year: 2026, views: 80, published: true },
{ title: "スキル一覧の表", year: 2025, views: 40, published: false },
];タイトルだけの文字列から、1 件ごとに複数の情報を持つオブジェクトの配列になりました。この章で身に付けた道具を並べておきます。
| 学んだこと | 書き方 | この回で使うか |
|---|---|---|
| オブジェクトの作成と読み取り | work.title | 使う |
| 複数の値をまとめて返す | return { count, totalViews } | 使う |
const と参照 | 中身は変えられる | 考え方として使う |
| 安全な取り出し | work?.views | 使う |
| 分割代入 | const { title } = work | 使ってもよい |
| スプレッド | [...works, newWork] | 使ってもよい |
| 検索 | works.find(...) | 使ってもよい |
今回書くのは集計関数です。1 本の関数の中に、この章のほぼ全てが顔を出します。
データを「配列に詰めた文字列」から「オブジェクトの配列」に変える作業を、データの構造化と呼びます。ここを越えると、並べ替えも絞り込みも集計も、同じ形のコードで書けるようになります。実務のコードの大半はこの形をしています。
何を集計するか決める
集計関数を書くときの最初の仕事は、返す形を先に決めることです。
{
count: 3, // 作品の件数
totalViews: 240, // 閲覧数の合計
latest: "自己紹介カード", // いちばん新しい作品のタイトル
}キーの名前と型を先に決めておくと、実装中に迷いません。関数の中で何を計算するかは、この形から逆算して決まります。
countは配列のlengthをそのまま使えますtotalViewsはループで足し上げますlatestはyearが最大の作品を探して、そのtitleを取り出します
集めるものは 1 回のループで集める
3 つの値を求めるからといって、ループを 3 回まわす必要はありません。1 回のループで全部集めるほうが速く、読みやすくもあります。
function summarizeWorks(works) {
let totalViews = 0;
let latest = null;
let latestYear = -Infinity;
for (const work of works) {
totalViews += work.views;
if (work.year > latestYear) {
latestYear = work.year;
latest = work.title;
}
}
return { count: works.length, totalViews, latest };
}latestYear は「これまでで最も新しい年」を覚えておくための変数です。最初は -Infinity にしておくと、どんな年が来ても必ず 1 件目で更新されます。0 を初期値にしても実用上は問題ありませんが、-Infinity のほうが「まだ何も見ていない」という意図がはっきりします。
最大値を求めるコードは、初期値の決め方がすべてです。「あり得ないほど小さい値から始める」か「1 件目を初期値にする」か。前者のほうが分岐が少なくて済みます。
そろっていないデータに耐えさせる
ここまでのコードは、全ての作品に views と year と title がそろっている前提です。しかし現実のデータは、たいていどこかが欠けています。下書き中の作品には views がまだ無いかもしれません。
const works = [
{ title: "自己紹介カード", year: 2026 }, // views が無い
];
console.log(summarizeWorks(works).totalViews); // NaNwork.views は undefined なので、0 + undefined の計算結果は NaN になります。一度 NaN が混ざると、その後どれだけ足しても NaN のままです。画面には「NaN 回」と表示されます。
第 3 章の ?? が効きます。
totalViews += work.views ?? 0;views が無いときだけ 0 を使う、という意味です。|| ではなく ?? を選ぶ理由は、views が本当に 0 の場合にもそのまま 0 を使いたいからです。今回はどちらでも同じ結果になりますが、?? を選ぶ習慣を付けてください。
year も同じように守ります。
const year = work.year ?? 0;
if (year > latestYear) { ... }空配列のときに何を返すか
集計関数でいちばん大事なのが、この決めごとです。まだ作品を 1 つも登録していないユーザーは必ず存在します。
summarizeWorks([]);このとき何が返るべきでしょうか。count は 0、totalViews も 0 で異論はないはずです。問題は latest です。ループが 1 度もまわらないので、初期値がそのまま返ります。
| 初期値の候補 | latest の戻り値 | 呼び出し側での扱い |
|---|---|---|
null | null | 「無い」ことを明示できる |
undefined | undefined | キーの書き忘れと区別が付かない |
"" | "" | 空文字と未登録が区別できない |
"なし" | "なし" | 表示文言が関数に混ざる |
今回は null を選びます。「値が無いことを意図して表している」という null 本来の意味に合っていて、呼び出し側も latest ?? "まだ作品がありません" の 1 行で表示を決められるからです。第 2 章で学んだ null と undefined の使い分けが、ここで具体的な設計判断になります。
表示用の文言を集計関数の中に書かないのは、意識しておく価値のある習慣です。集計は集計、表示は表示と分けておくと、日本語を英語に差し替えたいときに集計関数を触らずに済みます。
よくある間違い
lengthを数えるのに自分でカウンタを増やす
let count = 0;
for (const work of works) {
count += 1;
}間違いではありませんが、works.length で 1 行です。用意されているものは使いましょう。
- 最大値の初期値を 1 件目のデータから取らずに
0以外の中途半端な値にする
let latestYear = 2026; // これより古い作品しかないと latest が null のままになる初期値は「必ず負ける値」にします。年を扱うなら -Infinity か 0 です。
- 欠けているプロパティを守らずに足す
totalViews += work.views; // undefined が混ざると NaN
totalViews += work.views ?? 0; // これで守れるNaN はどんな比較でも false を返すため、if (totalViews > 0) のような判定もすり抜けます。入り口で止めるのがいちばん確実です。
ここまでの要点
返す形を先に決める。1 回のループで全部集める。欠けたプロパティは ?? で守り、空配列のときの答えを明示的に決める。
やってみよう
作品オブジェクトの配列を受け取り、集計結果のオブジェクトを返す関数を書きます。
- 引数
works(作品オブジェクトの配列) を受け取る countは作品の件数totalViewsはviewsの合計。viewsが無い作品は0として扱うlatestはyearがいちばん大きい作品のtitle。同じ年が並んだときは先に出てきたほうを採用する- 空配列のときは
{ count: 0, totalViews: 0, latest: null }を返す
キー名は count と totalViews と latest の 3 つです。テストは中身まで比較するので、つづりを正確に書いてください。
要件
- 関数名は
summarizeWorks、引数は作品オブジェクトの配列worksの 1 つ - 戻り値は
counttotalViewslatestの 3 つのキーを持つオブジェクト viewsが無い作品は0として合計することlatestはyearが最大の作品のtitle。同じ年なら先に出てきたほうを採用すること- 空配列のときは
{ count: 0, totalViews: 0, latest: null }を返すこと
入出力例
summarizeWorks([{"published":true,"title":"プロフィールサイト","views":120,"year":2024},{"published":true,"title":"自己紹介カード","views":80,"year":2026},{"published":false,"title":"スキル一覧の表","views":40,"year":2025}]) → {"count":3,"latest":"自己紹介カード","totalViews":240}
summarizeWorks([{"published":true,"title":"プロフィールサイト","views":120,"year":2026}]) → {"count":1,"latest":"プロフィールサイト","totalViews":120}
summarizeWorks([]) → {"count":0,"latest":null,"totalViews":0}
summarizeWorks([{"title":"自己紹介カード","year":2026},{"title":"スキル一覧の表","views":40,"year":2025}]) → {"count":2,"latest":"自己紹介カード","totalViews":40}
summarizeWorks([{"title":"プロフィールサイト","views":10,"year":2026},{"title":"自己紹介カード","views":5,"year":2026}]) → {"count":2,"latest":"プロフィールサイト","totalViews":15}