javascriptで等価性判定

javascriptでは、いくつかの等価性判定の演算子、メソッドがある。
等価性判定の方法をざっと纏めておく。
参考:等価性の比較と同一性

文字列、数値、真偽値

等価演算子「==」だと、型が異なる場合には暗黙の型変換をして判定される。

console.log(1 == '1');  // true
console.log(0 == '');   // true
console.log(1 == true); // true

等価演算子では、抽象等価比較アルゴリズムを用いて比較するらしいが、この辺を意識してプログラムを組むのも難しいので、通常 「==」は用いない。
厳密等価演算子「===」を用いるのが一般的。

console.log(1 === '1');  // false
console.log(0 === '');   // false
console.log(1 === true); // false

console.log(1 === 1);       // true
console.log(0 === 0);       // true
console.log('1' === '1');   // true
console.log(true === true); // true

null

厳密等価演算子「===」で比較。

const a = null;
console.log(a === null);       // true
console.log(a === 0);          // false
console.log(a === undefined);  // false

等価演算子だと、undefinedでもnullと等価だと判定されちゃう。

let b; // undefined
console.log(b == null); // true

非数

NaN(非数、Not a Number)の場合には厳密等価演算子でもfalseになること。

console.log(NaN == NaN);   // false
console.log(NaN === NaN);  // false

しようがないので、別のやり方で判定する必要がある。
そこで、isNaN関数があるので、それを用いることにしようとしても、ここでも暗黙の型変換の罠がある。

console.log(isNaN(NaN));     // true
console.log(isNaN('hoge'));  // true <- 文字列も非数と判定

厳密なNaNの判定については、Number.isNaNで行う。

console.log(Number.isNaN(NaN));     // true
console.log(Number.isNaN('hoge'));  // false

なお、配列のメソッドで非数が判定できないことがあるので注意。

// indexOfだと、NaNが見つけられない
[1, 2, NaN].indexOf(NaN);      // -1

// includesだと、NaNがあるか判定できる
[1, 2, NaN].includes(NaN);      // true
[1, 2, 'three'].includes(NaN);  // false

// findIndexでNumber.isNaNでテストすれば、NaNの位置がわかる
[1, 2, NaN].findIndex(n => Number.isNaN(n));      // 2
[1, 2, 'three'].findIndex(n => Number.isNaN(n));  // -1

配列(要素がプリミティブ値のみの場合)

等価演算子、厳密等価演算子だと参照の比較(同一性判定)なので、要素の中身の比較はされない。

const arr1 = [1, 2, 3];
const arr2 = [1, 2, 3];
console.log(arr1 == arr2);  // false
console.log(arr1 === arr2); // false

なので、中身の比較をして等価性判定するためにはちょっと工夫が必要。

for文で要素の中身を比較して頑張ろうとすると、以下になる。

function equalArray(arr1, arr2) {
    const equalValue = (x, y) => {
        return x === y || (Number.isNaN(x) && Number.isNaN(y));
    };
    if (arr1.length !== arr2.length) {
        return false;
    }
    for (let i = 0; i < arr1.length; i++) {
        if (!equalValue(arr1[i], arr2[i])) {
            return false;
        }
    }
    return true;
}

console.log(equalArray([1, 2, 3], [1, 2, 3]));        // true
console.log(equalArray([1, 2, 3], [1, 2, 'three']));  // false
console.log(equalArray([1, 2, 3], [1, 2, 3, 4]));     // false
console.log(equalArray([1, 2, NaN], [1, 2, NaN]));    // true
console.log(equalArray([1, 2, NaN], [1, 2, 3]));      // false

もっと簡単に、JSON.stringifyで文字列に直して比較しても大体OKなのだが、NaN、undefined、InfinityなどのJSONに対応していない値はnullに変換されてしまうため、厳密にそれらを区別したい場合は注意が必要。

function equalObject(arr1, arr2) {
    return JSON.stringify(arr1) === JSON.stringify(arr2);
}

console.log(equalObject([1, 2, 3], [1, 2, 3]));        // true
console.log(equalObject([1, 2, 3], [1, 2, 'three']));  // false
console.log(equalObject([1, 2, 3], [1, 2, 3, 4]));     // false
console.log(equalObject([1, 2, NaN], [1, 2, NaN]));    // true
console.log(equalObject([1, 2, NaN], [1, 2, 3]));      // false
console.log(equalObject([1, 2, NaN], [1, 2, null]));   // true ★ NaNはnullに変換される

オブジェクト(要素がプリミティブ値のみの場合)

無効なJSON値(NaN、undefined、Infinity)が要素に無ければ、JSON.stringifyで文字列に変換しての比較でいいのだが、そのまま比較するだけではキーの順序が揃っているもののみが等価と判定されてしまう。

console.log(equalObject({ a: '1', b: 2 }, { a: '1', b: 2 }));  // true
console.log(equalObject({ b: 2, a: '1' }, { a: '1', b: 2 }));  // false

Object.entriesでキーと値の配列に変換して、ソートした後、JSON.stringifyで比較すると、キーの順序が揃ってなくても等価と判定できる。

function equalObject2(obj1, obj2) {
    return JSON.stringify(Object.entries(obj1).sort())
        === JSON.stringify(Object.entries(obj2).sort());
}

console.log(equalObject2({ a: '1', b: 2 }, { a: '1', b: 2 }));  // true
console.log(equalObject2({ b: 2, a: '1' }, { a: '1', b: 2 }));  // true

無効なJSON値が含まれてて、厳密に判定したければ、リストの場合と同じようにfor文で要素を比較して頑張る。

function equalObject3(obj1, obj2) {
    const equalValue = (x, y) => {
        return x === y || (Number.isNaN(x) && Number.isNaN(y));
    };
    const arr1 = Object.entries(obj1).sort();
    const arr2 = Object.entries(obj2).sort();

    if (arr1.length !== arr2.length) {
        return false;
    }
    for (let i = 0; i < arr1.length; i++) {
        if (!equalValue(arr1[i][0], arr2[i][0])
            || !equalValue(arr1[i][1], arr2[i][1])) {
            return false;
        }
    }
    return true;
}

console.log(equalObject3({ b: 2, a: '1', c: null }, { a: '1', b: 2, c: NaN }));  // true

一般的な配列、オブジェクト

オブジェクトの要素がオブジェクトや配列だったり、配列の要素がオブジェクトだったりを考慮する場合、ここまでの応用でできないことはないが、ライブラリを使う方が安全。
lodash もしくは underscorejsで isEqual関数が提供されている。

console.log(_.isEqual(
    { a: '1', b: [2, { c: 3, d: 4 }] },
    { b: [2, { d: 4, c: 3 }], a: '1' }
));  // true

おわりに

  • 非数NaNの扱いには注意。
  • 配列やオブジェクトの比較は、だいたいはJSON.stringifyでOK(オブジェクトの場合はキー順序を考慮する必要あり)。ただし、無効なJSON値(NaN、undefined、Infinity)が要素に含まれている場合は注意。
  • より一般的な配列やオブジェクトの比較は、lodash もしくは underscorejsで比較すればよい。

以上です。