LoginSignup
6
6

More than 5 years have passed since last update.

JavaScript で配列やオブジェクトなどが同じ内容か比較する

Posted at

isSame(obj1, obj2) みたいなのが欲しい

2 つの内容が同じか比較がしたくなったけど、大変だったのでメモ。

やりたいこと.js
var obj1 = {a:'', b:0, c[true, false], d:{x:1}};
var obj2 = {a:'', b:0, c[true, false], d:{x:0}};

// isSame() を作って、obj1 と obj2 が違う内容ならエラーを投げたい
if (!isSame(obj1, obj2)) throw new Error();

つくったもの

JavaScript の標準オブジェクトなら、配列などでネストされていても同じ内容か判定する isSame() ができた。

つくったもの.js
/**
 * 2つの値の内容が同じか判定する関数  ※ 「0 と -0」 や 「JavaObject の内容」は区別しません。
 *
 * @param {*} val1 - 比較対象の値
 * @param {*} val2 - 比較対象の値
 * @return {boolean} 同じなら true、違うなら false を返す
 */
function isSame(val1, val2) {
  // 型を判定する関数を使って、val1 と val2 の両方が判定対象の型なら true を返す関数を定義
  var test = function(checkFunction) {
    return checkFunction.call(null, val1) && checkFunction.call(null, val2);
  }

  // 型が違えば false
  if (Object.prototype.toString.call(val1) !== Object.prototype.toString.call(val2)) return false;

  // 配列型なら isSameArray_() で比較
  if (test(isArray)) return isSameArray_(val1, val2);

  // オブジェクト型なら isSameObject_() で比較
  if (test(isObject)) return isSameObject_(val1, val2);

  // JSON.stringify() で判定できない型は、String 型に変換して比較
  if (test(isUndefined) || test(isDate) || test(isFunction) || test(isRegExp) || test(isError)) return val1 + '' === val2 + '';

  // 型は同じだが(Number 型)、判定が必要な値は String 型に変換して比較
  if (isNaN(val1) || isNaN(val2) || isInfinity(val1) || isInfinity(val2)) return val1 + '' === val2 + '';

  // JSON.stringify() で判定できる型なら比較
  if (test(isNull) || test(isString) || test(isNumber) || test(isBoolean)) return JSON.stringify(val1) === JSON.stringify(val2);

  // JavaObject (Google Apps Script のオブジェクト)を簡易比較
  if (test(isJavaObject)) return val1 + '' === val2 + '';

  // 上記以外は false
  return false;
}

/**
 * 2つの2次元配列の要素が同じか判定する関数  ※ 配列の並び順も含めて判定します。
 *
 * @param {Array} arr1 - 比較対象の2次元配列
 * @param {Array} arr2 - 比較対象の2次元配列
 * @return {boolean} 同じなら true、違うなら false を返す
 */
function isSameArray_(arr1, arr2) {
  // 要素数が異なれば false を返却
  if (arr1.length !== arr2.length) return false;

  // 空の配列同士なら true を返却
  if (arr1.length === 0) return true;

  // 各要素を走査
  return arr1.every(function(elem1, idx) {
    var elem2 = arr2[idx];
    return isSame(elem1, elem2);
  });
}

/**
 * 2つのオブジェクトの内容が同じか判定する関数
 *
 * @param {Object} obj1 - 比較対象のオブジェクト
 * @param {Object} obj2 - 比較対象のオブジェクト
 * @return {boolean} 同じなら true、違うなら false を返す
 */
function isSameObject_(obj1, obj2) {
  // キーを取得してソート  ※ sort() は破壊的なので slice() で複製してから実行
  var keysOfObj1 = Object.keys(obj1).slice().sort();
  var keysOfObj2 = Object.keys(obj2).slice().sort();

  // キー同士が異なれば false を返却
  if (!isSameArray_(keysOfObj1, keysOfObj2)) return false;

  // 空のオブジェクト同士なら true を返却
  if (keysOfObj1.length === 0) return true;

  // キー毎に各プロパティを走査
  return keysOfObj1.every(function(key) {
    var val1 = obj1[key];
    var val2 = obj2[key];
    return isSame(val1, val2);
  });
}


/////////////////////////////////////////////////
// 型をチェックする関数
/////////////////////////////////////////////////

/**
 * NaN か判定する関数
 */
function isNaN(value) {
  return Number.isNaN(value);
}

/**
 * Infinity か判定する関数
 */
function isInfinity(value) {
  return value === Infinity || value === -Infinity;
}

/**
 * 未定義か判定する関数
 */
function isUndefined(value){
  return typeof value === "undefined" ? true : false;
}

/**
 * null か判定する関数
 */
function isNull(value){
  return value === null ? true : false;
}

/**
 * 値が特定の型かどうか判定する関数
 * http://bonsaiden.github.io/JavaScript-Garden/ja/#types.typeof
 */
function is_(type, obj){
  var clas = Object.prototype.toString.call(obj).slice(8, -1);
  return obj !== undefined && obj !== null && clas === type;
}

/**
 * 値が String 型かどうか判定する関数
 */
function isString(value){
  return is_('String', value);
}

/**
 * 値が Number 型かどうか判定する関数
 */
function isNumber(value){
  return is_('Number', value);
}

/**
 * 値が Boolean 型かどうか判定する関数
 */
function isBoolean(value){
  return is_('Boolean', value);
}

/**
 * 値が Date 型かどうか判定する関数
 */
function isDate(value){
  return is_('Date', value);
}

/**
 * 値が Error 型かどうか判定する関数
 */
function isError(value){
  return is_('Error', value);
}

/**
 * 値が Array 型かどうか判定する関数
 */
function isArray(value){
  return is_('Array', value);
}

/**
 * 値が Function 型かどうか判定する関数
 */
function isFunction(value){
  return is_('Function', value);
}

/**
 * 値が RegExp 型かどうか判定する関数
 */
function isRegExp(value){
  return is_('RegExp', value);
}

/**
 * 値が Object 型かどうか判定する関数
 */
function isObject(value){
  return is_('Object', value);
}

/**
 * 値が Javasctip でラップされた Java オブジェクトかどうか判定する関数
 */
function isJavaObject(value) {
  return is_('JavaObject', value);
}


/////////////////////////////////////////////////
// ポリフィル
/////////////////////////////////////////////////

/**
 * ポリフィル: 引数として与えた数がNaNかどうかの真偽値を返します。
 * https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Number/isNaN
 */
Number.isNaN = Number.isNaN || function(value) {
  return typeof value === "number" && value !== value;
}

※ JavaObject は、「GAS のオブジェクト」と「JavaScript の素のオブジェクト」と区別するために使った。
 GAS のオブジェクトを Object.prototype.toString.call() でオブジェクトクラスの検出すると、JavaObject となったので、どの GAS の標準クラスでも判定できるはず。
 (SpreadsheetApp の Spreadsheet 型や Sheet 型、Utilities の Blob 型でのみ検証)

テスト結果

ちゃんと動いてそう。

テスト.js
function test_isSame() {
  // 一意に判定したい値を定義
  var testHash = {
    'undefined'  : [undefined],
    'null'       : [null],
    'string'     : ['', 'undefined', 'null', 'NaN', 'Infinity', '-Infinity', '0', '1', 'true', 'false', 'Sun Jan 01 2017 00:00:00 GMT+0900 (JST)', 'function(val) {return val;}', '/\d+/', 'Error: ', '[]', '{}', 'Logger'],
    'number'     : [0, -1, 1, 2*3, NaN, Infinity, -Infinity],
    'boolean'    : [true, false],
    'date'       : [new Date(2017, 0, 1)],
    'function'   : [function(val) {return val;}, function(arg1, arg2) {return arg1 + arg2 + ''}],
    'regExp'     : [/\d+/, /\^\s*$/],
    'error'      : [new Error(), new Error('err01'), new TypeError('err01')],
    'array'      : [[], ['0'], ['1'], [0], [1], [NaN], [true], [false], [null], [undefined], [[[null, undefined]]], [[[undefined, null]]]],
    'object'     : [{}, {'x':0,'y':0}, {'x':0,'z':0}, {'x':0,'z':1}, {'x':1, 'y':{'z':{'a':undefined}}}, {'x':1, 'y':{'z':{'a':null}}}],
    'javaObject' : [Logger.log(''), Utilities.newBlob('')]
  };

  // testHash を配列に変換  ex. {key1:[value1], key2:[valu2], ...} -> [value1, value2]
  var testArr = Object.keys(testHash).reduce(function(resultArr, key) {
    return resultArr.concat(testHash[key]);
  }, []);

  // エラーが出たら失敗
  var result = testArr.reduce(function(parentHash, e1, i1, arr) {
    parentHash[i1 + ':' + e1] = arr.map(function(e2, i2) {
      if (i1 === i2 && isSame(e1, e2))  return true;           // 自分自身との比較が true なら何もしない
      if (i1 !== i2 && !isSame(e1, e2)) return true;           // 自分以外との比較が false なら何もしない
      throw new Error(i1 + ':' + e1 + ' / ' + i2 + ':' + e2);  // それ以外はエラー
    });
    return parentHash;
  }, {});

  return result;
}
6
6
1

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
6
6