テーマの整理:「差分検出」とは何か
「差分検出」は、“前の配列”と“今の配列”を比べて、「何が増えたか」「何が消えたか」「何が変わったか」をはっきりさせるためのテクニックです。 業務だと、次のような場面でほぼ必ず出てきます。
前回のユーザー一覧と今回の一覧の違いをログに残したい。 画面の一覧で「追加された行」「削除された行」「更新された行」をハイライトしたい。 DB に同期するときに、「新規」「更新」「削除」を判定したい。
ここでは、初心者でも扱いやすい「配列差分検出」のユーティリティを、例題付きでかみ砕いて説明します。
基本コンセプト:ID を軸に「前」と「今」を比べる
なぜ「ID」が大事なのか
差分検出でいちばん大事なのは、「同じものかどうかを判定する軸」です。 業務データなら、だいたい「id」や「コード」などの一意なキーがあるはずです。
この「id」を使って、
前の配列にあって今の配列にない → 削除された。 前になくて今にある → 追加された。 両方にあるが中身が違う → 更新された。
というふうに差分を分類していきます。
差分検出ユーティリティの基本形
まずは、id をキーにしたシンプルな差分検出関数を作ります。
function diffById(prev, next) {
const prevMap = new Map(prev.map((item) => [item.id, item]));
const nextMap = new Map(next.map((item) => [item.id, item]));
const added = [];
const removed = [];
const updated = [];
const unchanged = [];
for (const [id, nextItem] of nextMap.entries()) {
const prevItem = prevMap.get(id);
if (!prevItem) {
added.push(nextItem);
continue;
}
const isSame =
JSON.stringify(prevItem) === JSON.stringify(nextItem);
if (isSame) {
unchanged.push(nextItem);
} else {
updated.push({ before: prevItem, after: nextItem });
}
}
for (const [id, prevItem] of prevMap.entries()) {
if (!nextMap.has(id)) {
removed.push(prevItem);
}
}
return { added, removed, updated, unchanged };
}
JavaScriptここでの重要ポイントをかみ砕いて説明します。
prev は「前の配列」、next は「今の配列」。 prevMap と nextMap は、「id → 要素」を引けるようにした Map。 added は「新しく増えた要素」、removed は「消えた要素」。 updated は「id は同じだが中身が変わった要素」、unchanged は「全く同じ要素」。 中身の比較には、初心者向けに分かりやすい JSON.stringify を使っている(厳密にはもっと工夫の余地あり)。
この関数が返すのは、「差分の分類結果」です。
例題 1:ユーザー一覧の差分を検出する
シナリオ
前回のユーザー一覧と今回のユーザー一覧がある。 「追加されたユーザー」「削除されたユーザー」「更新されたユーザー」を知りたい。
コード
const prevUsers = [
{ id: 1, name: "Alice", active: true },
{ id: 2, name: "Bob", active: false },
{ id: 3, name: "Carol", active: true },
];
const nextUsers = [
{ id: 1, name: "Alice", active: true }, // 変更なし
{ id: 2, name: "Bob Jr.", active: false }, // 名前変更
{ id: 4, name: "Dave", active: true }, // 新規
];
const diff = diffById(prevUsers, nextUsers);
console.log("added:", diff.added);
console.log("removed:", diff.removed);
console.log("updated:", diff.updated);
console.log("unchanged:", diff.unchanged);
JavaScriptここで起きていることを言葉で追うと、
id=4 は前になくて今にある → added。 id=3 は前にあって今にない → removed。 id=2 は両方にあるが name が変わっている → updated。 id=1 は両方にあり、中身も同じ → unchanged。
というふうに、差分がきれいに分類されます。
例題 2:差分を使って「DB 同期用の指示」を作る
シナリオ
画面側で編集された一覧を、DB に反映したい。 差分から「INSERT すべきもの」「UPDATE すべきもの」「DELETE すべきもの」を作りたい。
コード
function buildSyncCommands(prev, next) {
const diff = diffById(prev, next);
const inserts = diff.added.map((item) => ({
type: "INSERT",
payload: item,
}));
const updates = diff.updated.map(({ before, after }) => ({
type: "UPDATE",
id: before.id,
payload: after,
}));
const deletes = diff.removed.map((item) => ({
type: "DELETE",
id: item.id,
}));
return [...inserts, ...updates, ...deletes];
}
const prevUsers = [
{ id: 1, name: "Alice", active: true },
{ id: 2, name: "Bob", active: false },
];
const nextUsers = [
{ id: 1, name: "Alice", active: true },
{ id: 3, name: "Carol", active: true },
];
const commands = buildSyncCommands(prevUsers, nextUsers);
console.log(commands);
JavaScriptここでは、
差分検出の結果をもとに、「DB に対して何をすべきか」を明示的なコマンドに変換している。 INSERT は added、UPDATE は updated、DELETE は removed から作る。
という流れになっています。
差分検出は、「何が変わったか」を知るだけでなく、 「何をすべきか」に直結する情報として使えるのがポイントです。
例題 3:画面で「追加・削除・更新」をハイライトする
シナリオ
前回の一覧と今回の一覧を比べて、 追加された行は緑、削除された行は赤、更新された行は黄色で表示したい。
コード(概念的な例)
const prevUsers = [
{ id: 1, name: "Alice", active: true },
{ id: 2, name: "Bob", active: false },
];
const nextUsers = [
{ id: 1, name: "Alice", active: true },
{ id: 2, name: "Bob Jr.", active: false },
{ id: 3, name: "Carol", active: true },
];
const diff = diffById(prevUsers, nextUsers);
const rows = nextUsers.map((user) => {
const isAdded = diff.added.some((u) => u.id === user.id);
const isUpdated = diff.updated.some(({ after }) => after.id === user.id);
let status = "normal";
if (isAdded) status = "added";
else if (isUpdated) status = "updated";
return { ...user, status };
});
console.log(rows);
JavaScriptここでは、
nextUsers をベースに行を作りつつ、「この行は added か updated か」を差分から判定している。 status に応じて、画面側で色を変えたりアイコンを出したりできる。
という使い方をしています。
差分検出は、「UI の表現」にも直結する情報になります。
差分検出で意識してほしい重要ポイント
「何をもって“同じもの”とみなすか」を決める
差分検出の設計でいちばん大事なのは、 「何をもって同じものとみなすか」です。
業務データなら、だいたい次のようなパターンがあります。
id が同じなら同じレコード。 コードやメールアドレスなど、ビジネス的なキーが同じなら同じ。
ここを曖昧にしたまま差分を取ると、 「本当は同じものなのに別物扱い」「本当は別物なのに同じ扱い」になってしまいます。
初心者向けには、
まずは「id」を軸にする。 もし id がないなら、「何が一意なキーになりうるか」をチームで決める。
というステップを必ず踏んでほしいです。
「中身の比較」をどこまでやるか
上の例では、簡単のために JSON.stringify(prevItem) === JSON.stringify(nextItem) で比較しましたが、 実務では「どのフィールドの変更を“更新”とみなすか」を決める必要があります。
例えば、
name や active が変わったら更新。 内部的なメタ情報(更新日時など)は差分として扱わない。
など、ビジネス的な意味に合わせて比較対象を絞ると、 「本当に意味のある変更だけ」を更新として扱えるようになります。
手を動かして「差分検出」の感覚をつかむ
次のコードを実行して、「added / removed / updated / unchanged」の動きを体感してみてください。
function diffById(prev, next) {
const prevMap = new Map(prev.map((item) => [item.id, item]));
const nextMap = new Map(next.map((item) => [item.id, item]));
const added = [];
const removed = [];
const updated = [];
const unchanged = [];
for (const [id, nextItem] of nextMap.entries()) {
const prevItem = prevMap.get(id);
if (!prevItem) {
added.push(nextItem);
continue;
}
const isSame =
JSON.stringify(prevItem) === JSON.stringify(nextItem);
if (isSame) {
unchanged.push(nextItem);
} else {
updated.push({ before: prevItem, after: nextItem });
}
}
for (const [id, prevItem] of prevMap.entries()) {
if (!nextMap.has(id)) {
removed.push(prevItem);
}
}
return { added, removed, updated, unchanged };
}
const prev = [
{ id: 1, name: "A" },
{ id: 2, name: "B" },
{ id: 3, name: "C" },
];
const next = [
{ id: 1, name: "A" }, // unchanged
{ id: 2, name: "B2" }, // updated
{ id: 4, name: "D" }, // added
];
const diff = diffById(prev, next);
console.log("added :", diff.added);
console.log("removed :", diff.removed);
console.log("updated :", diff.updated);
console.log("unchanged:", diff.unchanged);
JavaScriptログを見れば、
何が追加され、何が削除され、何が更新され、何が変わっていないかが、 とてもはっきり見えるはずです。
まとめ:差分検出ユーティリティで「変化を構造化して扱う」
「差分検出」は、 「前と今の違いを、ただ“なんとなく”ではなく、“構造化された情報”として扱うためのユーティリティ」です。
プロジェクトに例えば次のような形で置いておくイメージです。
export function diffById(prev, next) { ... }
export function buildSyncCommands(prev, next) { ... }
JavaScriptそして、
一覧データの更新や DB 同期、ログ・監査、UI のハイライトなど、 「変化に意味があるところ」では必ず差分検出を通す。
こう決めておくと、 「何が変わったか」「何をすべきか」がコードから読み取りやすくなり、 業務ロジックがぐっと整理されていきます。
差分検出は、“変化を怖がらずに、ちゃんと見て、ちゃんと扱う”ための道具です。 そこを意識してユーティリティを育てていくと、配列まわりのコードが一段落ち着いたものになります。
