何をしたいユーティリティか:「配列の差分抽出」
ここでの「差分抽出」は、「ある配列 A と配列 B を比べて、“どこが違うのか”を取り出す処理」です。
業務だと、例えばこんな場面で使います。
- 画面で「変更前」と「変更後」のデータを比べて、追加・削除・変更された項目を知りたい。
- マスタの「前回版」と「今回版」を比べて、増えたコード・減ったコードを一覧にしたい。
- 権限リストの「前の状態」と「今の状態」の差分をログに残したい。
ここでは、まず「プリミティブ値の配列の差分」、次に「オブジェクト配列の差分」という順で、段階的にかみ砕いていきます。
プリミティブ配列の差分:A にだけあるもの・B にだけあるもの
まずは「左右どちらにだけあるか」を出す
一番基本の差分は、次の 2 つです。
- A にだけある要素(B にはない)
- B にだけある要素(A にはない)
図でいうとこんなイメージです。
A: [1, 2, 3]
B: [2, 3, 4]
A にだけ: [1]
B にだけ: [4]
実装例:Set を使ったシンプル版
function diffPrimitive(left, right) {
const a = Array.isArray(left) ? left : [];
const b = Array.isArray(right) ? right : [];
const setA = new Set(a);
const setB = new Set(b);
const onlyInA = [];
const onlyInB = [];
for (const v of setA) {
if (!setB.has(v)) {
onlyInA.push(v);
}
}
for (const v of setB) {
if (!setA.has(v)) {
onlyInB.push(v);
}
}
return { onlyInA, onlyInB };
}
JavaScript重要なポイントをかみ砕いて説明する
Set は「重複のない集合」です。
差分を考えるとき、多くの場合「同じ値が何回あるか」ではなく「その値が存在するかどうか」だけ分かれば十分なので、まず Set に変換してしまうと考えやすくなります。
const setA = new Set(a);
const setB = new Set(b);
JavaScriptそのうえで、
- 「A の要素を順に見て、B にないものを onlyInA に入れる」
- 「B の要素を順に見て、A にないものを onlyInB に入れる」
という 2 回のループで差分を出しています。
実際の動き
diffPrimitive([1, 2, 3], [2, 3, 4]);
// { onlyInA: [1], onlyInB: [4] }
diffPrimitive(["A", "B", "C"], ["B", "C", "D"]);
// { onlyInA: ["A"], onlyInB: ["D"] }
diffPrimitive([1, 1, 2], [2, 2, 3]);
// { onlyInA: [1], onlyInB: [3] } // 重複は集合として扱う
JavaScript「共通部分」も含めた 3 分類にする
A だけ・B だけ・両方にある
差分をもう少しリッチにすると、「3 分類」にできます。
- A にだけある
- B にだけある
- 両方にある
function diffPrimitiveWithCommon(left, right) {
const a = Array.isArray(left) ? left : [];
const b = Array.isArray(right) ? right : [];
const setA = new Set(a);
const setB = new Set(b);
const onlyInA = [];
const onlyInB = [];
const inBoth = [];
for (const v of setA) {
if (setB.has(v)) {
inBoth.push(v);
} else {
onlyInA.push(v);
}
}
for (const v of setB) {
if (!setA.has(v)) {
onlyInB.push(v);
}
}
return { onlyInA, onlyInB, inBoth };
}
JavaScript実際の動き
diffPrimitiveWithCommon([1, 2, 3], [2, 3, 4]);
// { onlyInA: [1], onlyInB: [4], inBoth: [2, 3] }
JavaScript業務では、「共通部分はそのまま」「A だけは削除」「B だけは追加」といった処理をしたいことが多いので、この 3 分類はかなり使い勝手がいいです。
オブジェクト配列の差分:キーを使って比べる
「id が同じなら同じレコード」とみなす
業務では、こんな配列同士を比べることが多いです。
const before = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
];
const after = [
{ id: 2, name: "Bob (updated)" },
{ id: 3, name: "Carol" },
];
JavaScriptここで知りたいのは、例えばこんなことです。
- 追加されたレコード(after にだけある id)
- 削除されたレコード(before にだけある id)
- id は同じだが中身が変わったレコード
このとき、「何をもって同じとみなすか」を決める必要があります。
よくあるのは「id が同じなら同じレコード」とみなすパターンです。
id をキーにした差分抽出
function diffByKey(before, after, key) {
const a = Array.isArray(before) ? before : [];
const b = Array.isArray(after) ? after : [];
const mapBefore = new Map();
const mapAfter = new Map();
for (const item of a) {
if (item && typeof item === "object") {
mapBefore.set(item[key], item);
}
}
for (const item of b) {
if (item && typeof item === "object") {
mapAfter.set(item[key], item);
}
}
const added = [];
const removed = [];
const maybeUpdated = [];
for (const [id, itemAfter] of mapAfter.entries()) {
if (!mapBefore.has(id)) {
added.push(itemAfter);
} else {
const itemBefore = mapBefore.get(id);
maybeUpdated.push({ before: itemBefore, after: itemAfter });
}
}
for (const [id, itemBefore] of mapBefore.entries()) {
if (!mapAfter.has(id)) {
removed.push(itemBefore);
}
}
return { added, removed, maybeUpdated };
}
JavaScript重要なポイントを深掘りする
mapBefore と mapAfter は、「id → レコード」の対応表です。
mapBefore.set(item[key], item);
mapAfter.set(item[key], item);
JavaScriptこうしておくと、「この id は before にいたか? after にいるか?」を高速に調べられます。
そのうえで、
- after にだけいる id → 追加されたレコード
- before にだけいる id → 削除されたレコード
- 両方にいる id → 「更新された可能性のあるレコード」
として分類しています。
ここでは「更新されたかどうか」の判定はまだしていません。maybeUpdated に { before, after } のペアを入れておき、
あとで「本当に中身が変わっているか」を別途チェックするイメージです。
実際の動き
const before = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
];
const after = [
{ id: 2, name: "Bob (updated)" },
{ id: 3, name: "Carol" },
];
const diff = diffByKey(before, after, "id");
// diff.added → [{ id: 3, name: "Carol" }]
// diff.removed → [{ id: 1, name: "Alice" }]
// diff.maybeUpdated → [
// { before: { id: 2, name: "Bob" }, after: { id: 2, name: "Bob (updated)" } }
// ]
JavaScript「更新されたかどうか」を判定する
shallow な比較で十分な場合
maybeUpdated のペアについて、「本当に変わっているか」を判定する関数を用意します。
function isShallowEqual(a, b) {
if (a === b) return true;
if (!a || !b) return false;
if (typeof a !== "object" || typeof b !== "object") return false;
const keysA = Object.keys(a);
const keysB = Object.keys(b);
if (keysA.length !== keysB.length) return false;
for (const k of keysA) {
if (a[k] !== b[k]) return false;
}
return true;
}
JavaScriptこれを使って、「本当に更新されたものだけ」を抽出します。
function extractUpdated(maybeUpdatedPairs) {
const updated = [];
for (const pair of maybeUpdatedPairs) {
if (!isShallowEqual(pair.before, pair.after)) {
updated.push(pair);
}
}
return updated;
}
JavaScript実際の動き
const diff = diffByKey(before, after, "id");
const updated = extractUpdated(diff.maybeUpdated);
// updated → [
// { before: { id: 2, name: "Bob" }, after: { id: 2, name: "Bob (updated)" } }
// ]
JavaScriptこれで、「追加」「削除」「更新」の 3 種類の差分がきれいに取れます。
実務で意識してほしい設計のポイント
「何をもって同じとみなすか」を最初に決める
差分抽出は、「同じとは何か?」を決める作業でもあります。
- プリミティブ配列なら「値が同じなら同じ」。
- オブジェクト配列なら「id が同じなら同じ」。
- 場合によっては「code+version の組み合わせが同じなら同じ」。
これを曖昧にしたまま書き始めると、「このケースは別扱いにしたい」といった例外が増えていきます。
ユーティリティとして、
diffPrimitive/diffPrimitiveWithCommondiffByKeyextractUpdated
のように「ルールごとに関数を分ける」と、意図がコードに乗りやすくなります。
「差分をどう使うか」までセットで考える
差分を取る目的は、「差分を眺めて満足すること」ではなく、その先のアクションです。
例えば、
- 追加されたもの → DB に INSERT する
- 削除されたもの → DB から DELETE する
- 更新されたもの → DB を UPDATE する
あるいは、
- 追加・削除・更新の内容を監査ログに残す
- 画面に「何が変わったか」を表示する
といった使い方をします。
だからこそ、差分ユーティリティの戻り値は、
{
added: [...],
removed: [...],
updated: [...],
}
JavaScriptのように、「そのまま次の処理に渡しやすい形」にしておくと、後工程が楽になります。
「順序」をどう扱うか
ここまでの実装では、「順序」はあまり意識していません。
業務の差分では、「順序はどうでもよくて、存在するかどうかだけ知りたい」ことが多いからです。
もし「順序の変化」も差分として扱いたい場合は、
別途「インデックスの違い」を見るロジックが必要になりますが、
それは一段難しい話になるので、まずは「集合としての差分」を押さえるのがおすすめです。
少し手を動かして感覚をつかむ
コンソールで、次のようなコードを実際に打ってみてください。
diffPrimitive([1, 2, 3], [2, 3, 4]);
diffPrimitiveWithCommon([1, 2, 3], [2, 3, 4]);
const before = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
];
const after = [
{ id: 2, name: "Bob (updated)" },
{ id: 3, name: "Carol" },
];
const d = diffByKey(before, after, "id");
d;
extractUpdated(d.maybeUpdated);
JavaScript「どの要素が追加・削除・更新として扱われているか」を、自分の目で確認してみてください。
そのうえで、自分のプロジェクトに
export function diffPrimitive(...) { ... }
export function diffPrimitiveWithCommon(...) { ... }
export function diffByKey(...) { ... }
export function extractUpdated(...) { ... }
JavaScriptを置き、
「配列の差分を取りたくなったら、必ずこの“差分抽出ユーティリティ”を通す」
というルールを作ってみてください。
それだけで、あなたの「変更検知」や「同期処理」は、場当たり的な for 文から、意図と一貫性を備えた業務レベルの設計に近づいていきます。
