つくる 作品データを構造化する

生田 陸人
ゆめさくエンジニア / 現役ソフトウェアエンジニア
編集 LuaGate編集部

このレッスンで分かること

  • 作品データをオブジェクトの配列として設計できます
  • 配列をまわしながら件数と合計と最大値を同時に集められます
  • 空配列やプロパティ欠落といった「そろっていないデータ」に耐える関数を書けます

つくる 作品データを構造化する とは

第 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 はループで足し上げます
  • latestyear が最大の作品を探して、その 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 件目を初期値にする」か。前者のほうが分岐が少なくて済みます。

そろっていないデータに耐えさせる

ここまでのコードは、全ての作品に viewsyeartitle がそろっている前提です。しかし現実のデータは、たいていどこかが欠けています。下書き中の作品には views がまだ無いかもしれません。

const works = [ { title: "自己紹介カード", year: 2026 }, // views が無い ]; console.log(summarizeWorks(works).totalViews); // NaN

work.viewsundefined なので、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([]);

このとき何が返るべきでしょうか。count0totalViews0 で異論はないはずです。問題は latest です。ループが 1 度もまわらないので、初期値がそのまま返ります。

初期値の候補latest の戻り値呼び出し側での扱い
nullnull「無い」ことを明示できる
undefinedundefinedキーの書き忘れと区別が付かない
""""空文字と未登録が区別できない
"なし""なし"表示文言が関数に混ざる

今回は null を選びます。「値が無いことを意図して表している」という null 本来の意味に合っていて、呼び出し側も latest ?? "まだ作品がありません" の 1 行で表示を決められるからです。第 2 章で学んだ nullundefined の使い分けが、ここで具体的な設計判断になります。

表示用の文言を集計関数の中に書かないのは、意識しておく価値のある習慣です。集計は集計、表示は表示と分けておくと、日本語を英語に差し替えたいときに集計関数を触らずに済みます。

よくある間違い

  • length を数えるのに自分でカウンタを増やす
let count = 0; for (const work of works) { count += 1; }

間違いではありませんが、works.length で 1 行です。用意されているものは使いましょう。

  • 最大値の初期値を 1 件目のデータから取らずに 0 以外の中途半端な値にする
let latestYear = 2026; // これより古い作品しかないと latest が null のままになる

初期値は「必ず負ける値」にします。年を扱うなら -Infinity0 です。

  • 欠けているプロパティを守らずに足す
totalViews += work.views; // undefined が混ざると NaN totalViews += work.views ?? 0; // これで守れる

NaN はどんな比較でも false を返すため、if (totalViews > 0) のような判定もすり抜けます。入り口で止めるのがいちばん確実です。

この章のポイント

ここまでの要点 返す形を先に決める。1 回のループで全部集める。欠けたプロパティは ?? で守り、空配列のときの答えを明示的に決める。

やってみよう

作品オブジェクトの配列を受け取り、集計結果のオブジェクトを返す関数を書きます。

  1. 引数 works (作品オブジェクトの配列) を受け取る
  2. count は作品の件数
  3. totalViewsviews の合計。views が無い作品は 0 として扱う
  4. latestyear がいちばん大きい作品の title。同じ年が並んだときは先に出てきたほうを採用する
  5. 空配列のときは { count: 0, totalViews: 0, latest: null } を返す

キー名は counttotalViewslatest の 3 つです。テストは中身まで比較するので、つづりを正確に書いてください。

要件

  1. 関数名は summarizeWorks、引数は作品オブジェクトの配列 works の 1 つ
  2. 戻り値は count totalViews latest の 3 つのキーを持つオブジェクト
  3. views が無い作品は 0 として合計すること
  4. latestyear が最大の作品の title。同じ年なら先に出てきたほうを採用すること
  5. 空配列のときは { 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}

ヒント

main.js
main.js
学習モード

メモ

つくる 作品データを構造化する

⌘S で保存