LoginSignup
21
16

More than 5 years have passed since last update.

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

Last updated at Posted at 2012-06-30

[] == [] // => 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 の実装
21
16
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
21
16