JavaScript Tips | 配列ユーティリティ:完全一致判定

JavaScript JavaScript
スポンサーリンク

何をしたいユーティリティか:「完全一致判定」

ここでの「完全一致判定」は、「この2つの値(配列やオブジェクトを含む)が“中身まで含めてまったく同じか”を判定する処理です。
単なる === ではなく、配列の中のオブジェクトや、さらにその中の配列まで、再帰的に全部たどって比較するイメージです。

業務だと、例えば次のような場面で使います。
前回の検索条件オブジェクトと、今回の検索条件オブジェクトが完全に同じか知りたい。
フォームの入力状態が、初期状態から一切変わっていないか判定したい。
キャッシュしているレスポンスと、新しく取得したレスポンスが完全に同じなら処理をスキップしたい。

こういうときに、「完全一致判定ユーティリティ」があると、毎回同じロジックを安全に使い回せます。


「完全一致」とは何かをはっきりさせる

完全一致をきちんと定義すると、次のようになります。

同じ型であること。
プリミティブ値(数値・文字列・真偽値など)は === で等しいこと。
配列なら、長さが同じで、各インデックスの要素が「完全一致」であること。
オブジェクトなら、持っているキーの集合が同じで、各プロパティの値が「完全一致」であること。

つまり、「構造」と「値」がすべて一致している状態を指します。
{ id: 1, name: "A" }{ name: "A", id: 1 } は、キーの順番は違っても中身は同じなので「完全一致」とみなします。
[1, 2, 3][1, 3, 2] は、要素の順番が違うので「完全一致ではない」とみなします。


ステップ1:プリミティブ配列の完全一致判定

まずは一番シンプルな、「中身がプリミティブ値だけの配列」の完全一致から始めます。
これは前にやった「配列比較」の equalsArray と同じ考え方です。

function equalsArray(a, b) {
  if (a === b) {
    return true;
  }

  if (!Array.isArray(a) || !Array.isArray(b)) {
    return false;
  }

  if (a.length !== b.length) {
    return false;
  }

  for (let i = 0; i < a.length; i++) {
    if (a[i] !== b[i]) {
      return false;
    }
  }

  return true;
}
JavaScript

ここでは、「長さ」「順番」「値」がすべて同じかどうかを見ています。
[1, 2, 3][1, 2, 3] は true、[1, 2, 3][3, 2, 1] は false です。

ただし、これはあくまで「要素がプリミティブ値の場合」に限った話です。
次のステップでは、オブジェクトやネストした配列も含めた「本当の意味での完全一致」を作ります。


ステップ2:オブジェクトやネストも含めた完全一致判定 deepEqual

完全一致判定の本命は、「配列・オブジェクトを再帰的にたどって比較する関数」です。
ここでは deepEqual という名前で実装してみます。

function deepEqual(a, b) {
  if (a === b) {
    return true;
  }

  if (a === null || b === null) {
    return false;
  }

  if (typeof a !== "object" || typeof b !== "object") {
    return false;
  }

  if (Array.isArray(a) || Array.isArray(b)) {
    if (!Array.isArray(a) || !Array.isArray(b)) {
      return false;
    }
    if (a.length !== b.length) {
      return false;
    }
    for (let i = 0; i < a.length; i++) {
      if (!deepEqual(a[i], b[i])) {
        return false;
      }
    }
    return true;
  }

  const keysA = Object.keys(a);
  const keysB = Object.keys(b);

  if (keysA.length !== keysB.length) {
    return false;
  }

  for (const key of keysA) {
    if (!Object.prototype.hasOwnProperty.call(b, key)) {
      return false;
    }
    if (!deepEqual(a[key], b[key])) {
      return false;
    }
  }

  return true;
}
JavaScript

少し長いので、重要なポイントだけ丁寧にかみ砕きます。

まず a === b を最初に見るのは、「同じ参照」や「同じプリミティブ値」なら即 true でいいからです。
null は typeof すると “object” になってしまうので、先に null を弾いています。
どちらか一方でも「オブジェクトではない」なら、その時点で false(プリミティブ同士は === で既に判定済み)。

ここから先が「配列かオブジェクトか」の分岐です。

配列の場合は、両方が配列であることを確認し、長さを比較し、各インデックスを deepEqual で再帰的に比較します。
オブジェクトの場合は、キーの一覧を取り、キーの数が同じか、同じキーを持っているかを確認し、各プロパティの値を deepEqual で再帰的に比較します。

これで、「配列の中にオブジェクトがあり、その中に配列があり…」という構造でも、全部たどって完全一致かどうかを判定できます。


例題で deepEqual の感覚をつかむ

次のコードをイメージしてみてください。

const a1 = { id: 1, name: "A", tags: ["x", "y"] };
const a2 = { name: "A", id: 1, tags: ["x", "y"] };
const a3 = { id: 1, name: "A", tags: ["y", "x"] };

deepEqual(a1, a2); // true
deepEqual(a1, a3); // false
JavaScript

a1a2 は、プロパティの順番は違いますが、キーと値の組み合わせは同じなので true です。
a1a3 は、tags の配列の順番が違うので false です。

ここでのポイントは、「配列の順番も完全一致の対象」であることです。
もし「順番はどうでもよくて、要素の集合が同じなら OK」にしたいなら、それは「完全一致」ではなく「集合としての一致」なので、別のユーティリティとして切り出すほうが分かりやすくなります。


業務での具体的な使い方

検索条件オブジェクトの完全一致判定を例にします。

let prevCondition = null;

function shouldSearch(nextCondition) {
  const same = deepEqual(prevCondition, nextCondition);
  prevCondition = nextCondition;
  return !same;
}
JavaScript

ここでは、「前回とまったく同じ条件なら検索しない」「どこか1つでも違っていたら検索する」という判定をしています。
deepEqual があることで、「ネストしたオブジェクトや配列を含めて完全一致かどうか」を一行で書けます。

フォームの初期値と現在値を比較する場合も同じです。

const initialForm = {
  name: "",
  age: null,
  tags: [],
};

function isPristine(currentForm) {
  return deepEqual(initialForm, currentForm);
}
JavaScript

「一度も変更されていない状態かどうか」を、構造ごと比較して判定できます。


手を動かして試してみる

コンソールで、次のコードをそのまま貼って動かしてみてください。

function deepEqual(a, b) {
  if (a === b) {
    return true;
  }
  if (a === null || b === null) {
    return false;
  }
  if (typeof a !== "object" || typeof b !== "object") {
    return false;
  }

  if (Array.isArray(a) || Array.isArray(b)) {
    if (!Array.isArray(a) || !Array.isArray(b)) {
      return false;
    }
    if (a.length !== b.length) {
      return false;
    }
    for (let i = 0; i < a.length; i++) {
      if (!deepEqual(a[i], b[i])) {
        return false;
      }
    }
    return true;
  }

  const keysA = Object.keys(a);
  const keysB = Object.keys(b);

  if (keysA.length !== keysB.length) {
    return false;
  }

  for (const key of keysA) {
    if (!Object.prototype.hasOwnProperty.call(b, key)) {
      return false;
    }
    if (!deepEqual(a[key], b[key])) {
      return false;
    }
  }

  return true;
}

const a1 = { id: 1, name: "A", tags: ["x", "y"] };
const a2 = { name: "A", id: 1, tags: ["x", "y"] };
const a3 = { id: 1, name: "A", tags: ["y", "x"] };

console.log(deepEqual(a1, a2)); // true
console.log(deepEqual(a1, a3)); // false
JavaScript

「どの組み合わせが true になって、どれが false になるか」を自分の目で確かめてみてください。
そのうえで、自分のプロジェクトに

export function deepEqual(...) { ... }
JavaScript

のような関数を置き、「オブジェクトや配列が“完全に同じかどうか”を判定したくなったら、必ずこの“完全一致判定ユーティリティ”を通す」と決めてみてください。
それだけで、「なんとなく比較しているコード」から、「意図がはっきりした堅い比較ロジック」に一段レベルアップできます。

タイトルとURLをコピーしました