[1,2,3] === [1,2,3] で false なので deepEqual() を実装

More than 5 years have passed since last update.


[] == [] // => false

よく知られたことですが、{}[] リテラルで作られたもの同士は同値比較で false を返します

[] == []; // false

{} == {}; // false
[] === []; // false
({}) === ({}); // false

これは [] を new Array(), {}new Object() だと解釈すると自然と理解できると思います。つまりそれぞれ新しいオブジェクトなので、処理系内部的には違うメモリ空間にあるはずです。


でもうっかりやってしまう arr == [1, 2, 3]; の罠

そうとわかっていても、うっかりこうやってテストを書いてしまった経験がある人は多いんじないでしょうか

// ライブラリに書いた Array を作るメソッド

var makeArray = function () {
return Array.prototype.slice.call(arguments);
}

// QUnit でのテスト
test("a test", function() {
equal(makeArray(1, 2, 3), [1,2,3]); // test failed で「あれー?」ってなる
});

わりとやりがちかと思います。


QUnit にある deepEqual() メソッド

なので QUnit には deepEqual() メソッド が用意されており、オブジェクトや配列を再帰的に精査して「要素が同じなので true でいいか」という判定をします。

// 先のテストの書き直し

test("a test", function() {
deepEqual(makeArray(1, 2, 3), [1,2,3]); // test failed で「あれー?」ってなる
});

なお、 Jasmine では .toEqual() は deepEqual と同じく expected と actual の要素も見て判定します。

Node.js のための Should.js は内部的に QUnit とは挙動の仕様が違う deepEqual を private に実装していたりします。


自分で実装したい

上記のテストフレームワークを使うまでもなく、「いや、書き捨てで試したいんだけど」という場面は割とあるので、上のフレームワークとは実装と挙動に違いはあるものの、こう実装しておくと deepEqual() のようなものができます。

/**

* @param {*} x
* @return {boolean} "typeof x" is Primitive
*/

function isPrimitive(x) {
// ECMAScript の仕様上, null はプリミティブであるはずなんだけど
// typeof null => 'object' になってしまうので、
if (x === null) {
return true;
}
var type_expr = typeof x,
primitives = [
'undefined', 'boolean', 'number', 'string'
],
i, l;
for (i = 0, l = primitives.length; i < l; i++) {
if (type_expr === primitives[i]) {
return true;
}
}
return false;
}

/**
* @description function.toString() したものを正規化する
* @param {string} expr
* @return {string}
*/

function replaceSpaces(expr) {
return expr.replace(/\s*\{\s*/,
' { ').replace(/\s*\}\s*/,
' }').replace(/\s*,\s*/,
', ').replace(/\s*;\s*/,
'; ').replace(/\s+/, ' ');
}

/**
* @description 2つの function がほとんど同じかどうか
* @param {function(...*):*} a
* @param {function(...*):*} b
* @return {boolean}
*/

function isSameFunction(a, b) {
if (a === b) {
return true;
}
if (a.name !== b.name || a.length !== b.length) {
return false;
}
var as = replaceSpaces(a.toString()),
bs = replaceSpaces(b.toString());
return as === bs;
}

/**
* @param {*} a
* @param {*} b
* @return {boolean}
*/

function deepEq(a, b) {
var i, l, a_ps, b_ps;
// Same identifier, or alias
if (a === b) {
return true;
}
// Primitive
if (isPrimitive(a) && isPrimitive(b)) {
return a === b;
}
// Array
if (Array.isArray(a) && Array.isArray(b)) {
if (a.length === b.length) {
for (i = 0, l = a.length; i < l; i++){
if (!deepEq(a[i], b[i])) { // それぞれの要素を再帰で判定して、 false が来たら切り上げる
return false;
}
}
return true;
}
return false;
}
// Function
if (typeof a === 'function' && typeof b === 'function') {
return isSameFunction(a, b);
}
// Object
if (a.constructor === b.constructor) {
a_ps = Object.getOwnPropertyNames(a);
b_ps = Object.getOwnPropertyNames(b);
if (a_ps.length !== b_ps.length) {
return false;
}
for (i = 0, l = a_ps.length; i < l; i++) {
if (!deepEq(a[a_ps[i]], b[b_ps[i]])) {
return false;
}
}
return true;
}
return false;
}


とりあえずチェックできるよー

deepEqual([1,2,3], [1,2,3]); // true

deepEqual([1,[2,3]], [[1,2,]3]); // false
deepEqual({hoge:'hoge', huga:'huga'}, {hoge:'hoge', huga:'huga'}); // true

ソースは GitHub においてあります



  • object.js : ↓の eq.js で依存してる API


  • eq.js : deepEqual の実装