Как определить равенство для двух объектов JavaScript? - PullRequest
547 голосов
/ 14 октября 2008

Оператор строгого равенства сообщит вам, если два объекта типов равны. Однако, есть ли способ определить, равны ли два объекта, так же, как значение хеш-кода в Java?

Вопрос переполнения стека Есть ли какая-либо функция hashCode в JavaScript? похожа на этот вопрос, но требует более академического ответа. Сценарий, приведенный выше, демонстрирует, почему это необходимо, и мне интересно, есть ли какое-нибудь эквивалентное решение .

Ответы [ 55 ]

3 голосов
/ 19 ноября 2008

Я бы посоветовал не использовать хэширование или сериализацию (как предлагает решение JSON). Если вам нужно проверить, равны ли два объекта, вам нужно определить, что означает «равно». Может случиться так, что все элементы данных в обоих объектах совпадают, или это может быть то, что ячейки памяти должны совпадать (имеется в виду, что обе переменные ссылаются на один и тот же объект в памяти), или может быть, что только один элемент данных в каждом объекте должен совпадать.

Недавно я разработал объект, конструктор которого создает новый идентификатор (начиная с 1 и увеличивая на 1) каждый раз, когда создается экземпляр. Этот объект имеет функцию isEqual, которая сравнивает это значение id со значением id другого объекта и возвращает true, если они совпадают.

В этом случае я определил «равно» как означающее совпадение значений идентификатора. Учитывая, что каждый экземпляр имеет уникальный идентификатор, это можно использовать для реализации идеи о том, что совпадающие объекты также занимают одно и то же место в памяти. Хотя в этом нет необходимости.

3 голосов
/ 13 сентября 2016

Я столкнулся с той же проблемой и решил написать свое собственное решение. Но так как я хочу также сравнить массивы с объектами и наоборот, я создал общее решение. Я решил добавить функции в прототип, но их легко переписать в автономные функции. Вот код:

Array.prototype.equals = Object.prototype.equals = function(b) {
    var ar = JSON.parse(JSON.stringify(b));
    var err = false;
    for(var key in this) {
        if(this.hasOwnProperty(key)) {
            var found = ar.find(this[key]);
            if(found > -1) {
                if(Object.prototype.toString.call(ar) === "[object Object]") {
                    delete ar[Object.keys(ar)[found]];
                }
                else {
                    ar.splice(found, 1);
                }
            }
            else {
                err = true;
                break;
            }
        }
    };
    if(Object.keys(ar).length > 0 || err) {
        return false;
    }
    return true;
}

Array.prototype.find = Object.prototype.find = function(v) {
    var f = -1;
    for(var i in this) {
        if(this.hasOwnProperty(i)) {
            if(Object.prototype.toString.call(this[i]) === "[object Array]" || Object.prototype.toString.call(this[i]) === "[object Object]") {
                if(this[i].equals(v)) {
                    f = (typeof(i) == "number") ? i : Object.keys(this).indexOf(i);
                }
            }
            else if(this[i] === v) {
                f = (typeof(i) == "number") ? i : Object.keys(this).indexOf(i);
            }
        }
    }
    return f;
}

Этот алгоритм разбит на две части; Сама функция equals и функция для поиска числового индекса свойства в массиве / объекте. Функция find нужна только потому, что indexof находит только числа и строки, а не объекты.

Можно назвать так:

({a: 1, b: "h"}).equals({a: 1, b: "h"});

Функция возвращает true или false, в этом случае true. Алгоритм также позволяет сравнивать очень сложные объекты:

({a: 1, b: "hello", c: ["w", "o", "r", "l", "d", {answer1: "should be", answer2: true}]}).equals({b: "hello", a: 1, c: ["w", "d", "o", "r", {answer1: "should be", answer2: true}, "l"]})

Верхний пример вернет true, даже если свойства имеют другой порядок. Одна небольшая деталь, на которую нужно обратить внимание: этот код также проверяет наличие двух типов переменных одного и того же типа, поэтому «3» - это не то же самое, что и 3.

2 голосов
/ 28 августа 2010

Полезно считать два объекта равными, если они имеют одинаковые значения для всех свойств и рекурсивно для всех вложенных объектов и массивов. Я также считаю следующие два объекта равными:

var a = {p1: 1};
var b = {p1: 1, p2: undefined};

Аналогично, массивы могут иметь «отсутствующие» элементы и неопределенные элементы. Я бы тоже к ним относился:

var c = [1, 2];
var d = [1, 2, undefined];

Функция, которая реализует это определение равенства:

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

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

    if (a == b) {
        return true;
    }

    if (typeof a != 'object') {
        return false;
    }

    // null != {}
    if (a instanceof Object != b instanceof Object) {
        return false;
    }

    if (a instanceof Date || b instanceof Date) {
        if (a instanceof Date != b instanceof Date ||
            a.getTime() != b.getTime()) {
            return false;
        }
    }

    var allKeys = [].concat(keys(a), keys(b));
    uniqueArray(allKeys);

    for (var i = 0; i < allKeys.length; i++) {
        var prop = allKeys[i];
        if (!isEqual(a[prop], b[prop])) {
            return false;
        }
    }
    return true;
}

Исходный код (включая вспомогательные функции, generalType и uniqueArray): Unit Test и Test Runner здесь .

2 голосов
/ 21 декабря 2013

Это дополнение для всего вышеперечисленного, а не замена. Если вам нужно быстро сравнить мелкие объекты, не нужно проверять дополнительные рекурсивные случаи. Вот выстрел.

Для сравнения: 1) Равенство числа собственных свойств, 2) Равенство имен ключей, 3) Если bCompareValues ​​== true, Равенство соответствующих значений свойств и их типов (тройное равенство)

var shallowCompareObjects = function(o1, o2, bCompareValues) {
    var s, 
        n1 = 0,
        n2 = 0,
        b  = true;

    for (s in o1) { n1 ++; }
    for (s in o2) { 
        if (!o1.hasOwnProperty(s)) {
            b = false;
            break;
        }
        if (bCompareValues && o1[s] !== o2[s]) {
            b = false;
            break;
        }
        n2 ++;
    }
    return b && n1 == n2;
}
2 голосов
/ 29 ноября 2013

Для сравнения ключей для простых экземпляров объекта пары ключ / значение я использую:

function compareKeys(r1, r2) {
    var nloops = 0, score = 0;
    for(k1 in r1) {
        for(k2 in r2) {
            nloops++;
            if(k1 == k2)
                score++; 
        }
    }
    return nloops == (score * score);
};

После сравнения ключей достаточно простого дополнительного цикла for..in.

Сложность - O (N * N), где N - количество ключей.

Я надеюсь / думаю, что определенные мной объекты не будут содержать более 1000 свойств ...

2 голосов
/ 22 января 2019

Для тех из вас, кто использует NodeJS, есть удобный метод под названием isDeepStrictEqual в собственной библиотеке Util, который может достичь этого.

const util = require('util');

const foo = {
  hey: "ho",
  lets: "go"
}

const bar = {
  hey: "ho",
  lets: "go"
}

foo == bar // false
util.isDeepStrictEqual(foo, bar) // true

https://nodejs.org/api/util.html#util_util_isdeepstrictequal_val1_val2

2 голосов
/ 02 октября 2018

Я вижу ответы с кодом спагетти. Без использования сторонних библиотек это очень просто.

Сначала отсортируйте два объекта по ключам, указав их имена.

let objectOne = { hey, you }
let objectTwo = { you, hey }

// If you really wanted you could make this recursive for deep sort.
const sortObjectByKeyname = (objectToSort) => {
    return Object.keys(objectToSort).sort().reduce((r, k) => (r[k] = objectToSort[k], r), {});
}

let objectOne = sortObjectByKeyname(objectOne)
let objectTwo = sortObjectByKeyname(objectTwo)

Затем просто используйте строку для сравнения.

JSON.stringify(objectOne) === JSON.stringify(objectTwo)
2 голосов
/ 17 мая 2018

При условии, что порядок свойств в объекте не изменился.

JSON.stringify () работает для глубоких и неглубоких объектов обоих типов, не очень уверенный в аспектах производительности:

var object1 = {
  key: "value"
};

var object2 = {
  key: "value"
};

var object3 = {
  key: "no value"
};

console.log('object1 and object2 are equal: ', JSON.stringify(object1) === JSON.stringify(object2));

console.log('object2 and object3 are equal: ', JSON.stringify(object2) === JSON.stringify(object3));
2 голосов
/ 17 апреля 2015

Извлечение из моей личной библиотеки, которую я неоднократно использую для своей работы. Следующая функция представляет собой рекурсивный глубокий равный, который не проверяет

  • Класс равенства
  • Унаследованные значения
  • Значения строгого равенства

Я в основном использую это, чтобы проверить, получаю ли я равные ответы на различные реализации API. Где может возникнуть разница в реализации (например, строка против числа) и дополнительные нулевые значения.

Его реализация довольно проста и коротка (если все комментарии удалены)

/** Recursively check if both objects are equal in value
***
*** This function is designed to use multiple methods from most probable 
*** (and in most cases) valid, to the more regid and complex method.
***
*** One of the main principles behind the various check is that while
*** some of the simpler checks such as == or JSON may cause false negatives,
*** they do not cause false positives. As such they can be safely run first.
***
*** # !Important Note:
*** as this function is designed for simplified deep equal checks it is not designed
*** for the following
***
*** - Class equality, (ClassA().a = 1) maybe valid to (ClassB().b = 1)
*** - Inherited values, this actually ignores them
*** - Values being strictly equal, "1" is equal to 1 (see the basic equality check on this)
*** - Performance across all cases. This is designed for high performance on the
***   most probable cases of == / JSON equality. Consider bench testing, if you have
***   more 'complex' requirments
***
*** @param  objA : First object to compare
*** @param  objB : 2nd object to compare
*** @param  .... : Any other objects to compare
***
*** @returns true if all equals, or false if invalid
***
*** @license Copyright by eugene@picoded.com, 2012.
***          Licensed under the MIT license: http://opensource.org/licenses/MIT
**/
function simpleRecusiveDeepEqual(objA, objB) {
	// Multiple comparision check
	//--------------------------------------------
	var args = Array.prototype.slice.call(arguments);
	if(args.length > 2) {
		for(var a=1; a<args.length; ++a) {
			if(!simpleRecusiveDeepEqual(args[a-1], args[a])) {
				return false;
			}
		}
		return true;
	} else if(args.length < 2) {
		throw "simpleRecusiveDeepEqual, requires atleast 2 arguments";
	}
	
	// basic equality check,
	//--------------------------------------------
	// if this succed the 2 basic values is equal,
	// such as numbers and string.
	//
	// or its actually the same object pointer. Bam
	//
	// Note that if string and number strictly equal is required
	// change the equality from ==, to ===
	//
	if(objA == objB) {
		return true;
	}
	
	// If a value is a bsic type, and failed above. This fails
	var basicTypes = ["boolean", "number", "string"];
	if( basicTypes.indexOf(typeof objA) >= 0 || basicTypes.indexOf(typeof objB) >= 0 ) {
		return false;
	}
	
	// JSON equality check,
	//--------------------------------------------
	// this can fail, if the JSON stringify the objects in the wrong order
	// for example the following may fail, due to different string order:
	//
	// JSON.stringify( {a:1, b:2} ) == JSON.stringify( {b:2, a:1} )
	//
	if(JSON.stringify(objA) == JSON.stringify(objB)) {
		return true;
	}
	
	// Array equality check
	//--------------------------------------------
	// This is performed prior to iteration check,
	// Without this check the following would have been considered valid
	//
	// simpleRecusiveDeepEqual( { 0:1963 }, [1963] );
	//
	// Note that u may remove this segment if this is what is intended
	//
	if( Array.isArray(objA) ) {
		//objA is array, objB is not an array
		if( !Array.isArray(objB) ) {
			return false;
		}
	} else if( Array.isArray(objB) ) {
		//objA is not array, objB is an array
		return false;
	}
	
	// Nested values iteration
	//--------------------------------------------
	// Scan and iterate all the nested values, and check for non equal values recusively
	//
	// Note that this does not check against null equality, remove the various "!= null"
	// if this is required
	
	var i; //reuse var to iterate
	
	// Check objA values against objB
	for (i in objA) {
		//Protect against inherited properties
		if(objA.hasOwnProperty(i)) {
			if(objB.hasOwnProperty(i)) {
				// Check if deep equal is valid
				if(!simpleRecusiveDeepEqual( objA[i], objB[i] )) {
					return false;
				}
			} else if(objA[i] != null) {
				//ignore null values in objA, that objB does not have
				//else fails
				return false;
			}
		}
	}
	
	// Check if objB has additional values, that objA do not, fail if so
	for (i in objB) {
		if(objB.hasOwnProperty(i)) {
			if(objB[i] != null && !objA.hasOwnProperty(i)) {
				//ignore null values in objB, that objA does not have
				//else fails
				return false;
			}
		}
	}
	
	// End of all checks
	//--------------------------------------------
	// By reaching here, all iteration scans have been done.
	// and should have returned false if it failed
	return true;
}

// Sanity checking of simpleRecusiveDeepEqual
(function() {
	if(
		// Basic checks
		!simpleRecusiveDeepEqual({}, {}) ||
		!simpleRecusiveDeepEqual([], []) ||
		!simpleRecusiveDeepEqual(['a'], ['a']) ||
		// Not strict checks
		!simpleRecusiveDeepEqual("1", 1) ||
		// Multiple objects check
		!simpleRecusiveDeepEqual( { a:[1,2] }, { a:[1,2] }, { a:[1,2] } ) ||
		// Ensure distinction between array and object (the following should fail)
		simpleRecusiveDeepEqual( [1963], { 0:1963 } ) ||
		// Null strict checks
		simpleRecusiveDeepEqual( 0, null ) ||
		simpleRecusiveDeepEqual( "", null ) ||
		// Last "false" exists to make the various check above easy to comment in/out
		false
	) {
		alert("FATAL ERROR: simpleRecusiveDeepEqual failed basic checks");
	} else { 
		//added this last line, for SO snippet alert on success
		alert("simpleRecusiveDeepEqual: Passed all checks, Yays!");
	}
})();
2 голосов
/ 09 января 2018

Вот версия трюка stringify, которая меньше набирает текст и во многих случаях работает для тривиальных сравнений данных JSON.

var obj1Fingerprint = JSON.stringify(obj1).replace(/\{|\}/g,'').split(',').sort().join(',');
var obj2Fingerprint = JSON.stringify(obj2).replace(/\{|\}/g,'').split(',').sort().join(',');
if ( obj1Fingerprint === obj2Fingerprint) { ... } else { ... }
...