Сравнение объектов в JavaScript
Как лучше всего сравнивать объекты в JavaScript?
Пример:
var user1 = {name : "nerd", org: "dev"};
var user2 = {name : "nerd", org: "dev"};
var eq = user1 == user2;
alert(eq); // gives false
Я знаю, что два объекта равны, если они ссылаются на один и тот же объект, но есть ли способ проверить, имеют ли они одинаковые значения атрибутов?
Следующий способ работает для меня, но это единственная возможность?
var eq = Object.toJSON(user1) == Object.toJSON(user2);
alert(eq); // gives true
10 ответов
К сожалению, нет идеального способа, если вы не используете _proto_
рекурсивно и получить доступ ко всем не перечисляемым свойствам, но это работает только в Firefox.
Поэтому лучшее, что я могу сделать, это угадать сценарии использования.
1) Быстрый и ограниченный.
Работает, когда у вас есть простые объекты в стиле JSON без методов и узлов DOM внутри:
JSON.stringify(obj1) === JSON.stringify(obj2)
ПОРЯДОК свойств является ВАЖНЫМ, поэтому этот метод вернет false для следующих объектов:
x = {a: 1, b: 2};
y = {b: 2, a: 1};
2) Медленный и более общий.
Сравнивает объекты, не копаясь в прототипах, затем рекурсивно сравнивает проекции свойств, а также сравнивает конструкторы.
Это почти правильный алгоритм:
function deepCompare () {
var i, l, leftChain, rightChain;
function compare2Objects (x, y) {
var p;
// remember that NaN === NaN returns false
// and isNaN(undefined) returns true
if (isNaN(x) && isNaN(y) && typeof x === 'number' && typeof y === 'number') {
return true;
}
// Compare primitives and functions.
// Check if both arguments link to the same object.
// Especially useful on the step where we compare prototypes
if (x === y) {
return true;
}
// Works in case when functions are created in constructor.
// Comparing dates is a common scenario. Another built-ins?
// We can even handle functions passed across iframes
if ((typeof x === 'function' && typeof y === 'function') ||
(x instanceof Date && y instanceof Date) ||
(x instanceof RegExp && y instanceof RegExp) ||
(x instanceof String && y instanceof String) ||
(x instanceof Number && y instanceof Number)) {
return x.toString() === y.toString();
}
// At last checking prototypes as good as we can
if (!(x instanceof Object && y instanceof Object)) {
return false;
}
if (x.isPrototypeOf(y) || y.isPrototypeOf(x)) {
return false;
}
if (x.constructor !== y.constructor) {
return false;
}
if (x.prototype !== y.prototype) {
return false;
}
// Check for infinitive linking loops
if (leftChain.indexOf(x) > -1 || rightChain.indexOf(y) > -1) {
return false;
}
// Quick checking of one object being a subset of another.
// todo: cache the structure of arguments[0] for performance
for (p in y) {
if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) {
return false;
}
else if (typeof y[p] !== typeof x[p]) {
return false;
}
}
for (p in x) {
if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) {
return false;
}
else if (typeof y[p] !== typeof x[p]) {
return false;
}
switch (typeof (x[p])) {
case 'object':
case 'function':
leftChain.push(x);
rightChain.push(y);
if (!compare2Objects (x[p], y[p])) {
return false;
}
leftChain.pop();
rightChain.pop();
break;
default:
if (x[p] !== y[p]) {
return false;
}
break;
}
}
return true;
}
if (arguments.length < 1) {
return true; //Die silently? Don't know how to handle such case, please help...
// throw "Need two or more arguments to compare";
}
for (i = 1, l = arguments.length; i < l; i++) {
leftChain = []; //Todo: this can be cached
rightChain = [];
if (!compare2Objects(arguments[0], arguments[i])) {
return false;
}
}
return true;
}
Известные проблемы (ну, они имеют очень низкий приоритет, вероятно, вы их никогда не заметите):
- объекты с другой структурой прототипа, но с одинаковой проекцией
- функции могут иметь одинаковый текст, но ссылаться на разные замыкания
Тесты: проходит тесты из Как определить равенство для двух объектов JavaScript?,
Вот мое закомментированное решение в ES3 (подробности после кода):
Object.equals = function( x, y ) {
if ( x === y ) return true;
// if both x and y are null or undefined and exactly the same
if ( ! ( x instanceof Object ) || ! ( y instanceof Object ) ) return false;
// if they are not strictly equal, they both need to be Objects
if ( x.constructor !== y.constructor ) return false;
// they must have the exact same prototype chain, the closest we can do is
// test there constructor.
for ( var p in x ) {
if ( ! x.hasOwnProperty( p ) ) continue;
// other properties were tested using x.constructor === y.constructor
if ( ! y.hasOwnProperty( p ) ) return false;
// allows to compare x[ p ] and y[ p ] when set to undefined
if ( x[ p ] === y[ p ] ) continue;
// if they have the same strict value or identity then they are equal
if ( typeof( x[ p ] ) !== "object" ) return false;
// Numbers, Strings, Functions, Booleans must be strictly equal
if ( ! Object.equals( x[ p ], y[ p ] ) ) return false;
// Objects and Arrays must be tested recursively
}
for ( p in y ) {
if ( y.hasOwnProperty( p ) && ! x.hasOwnProperty( p ) ) return false;
// allows x[ p ] to be set to undefined
}
return true;
}
Разрабатывая это решение, я обратил особое внимание на угловые случаи, эффективность, но в то же время пытался найти простое решение, которое работает, надеюсь, с некоторой элегантностью. JavaScript допускает как нулевые, так и неопределенные свойства, и объекты имеют цепочки прототипов, которые могут приводить к очень разным поведениям, если их не проверять.
Сначала я решил расширить Object вместо Object.prototype, главным образом потому, что null не может быть одним из объектов сравнения и что я считаю, что null должен быть допустимым объектом для сравнения с другим. Есть также другие законные проблемы, отмеченные другими в отношении расширения Object.prototype относительно возможных побочных эффектов на код другого.
Особое внимание необходимо уделить тому, чтобы JavaScript позволял устанавливать свойства объекта неопределенными, т. Е. Существуют свойства, значения которых установлены неопределенными. Приведенное выше решение проверяет, что оба объекта имеют одинаковые свойства, для которых установлено значение undefined, чтобы сообщить о равенстве. Это может быть достигнуто только путем проверки существования свойств с помощью Object.hasOwnProperty (property_name). Также обратите внимание, что JSON.stringify() удаляет свойства, для которых установлено значение undefined, и поэтому при сравнении с использованием этой формы игнорируются свойства, для которых установлено значение undefined.
Функции следует считать равными, только если они имеют одну и ту же ссылку, а не только один и тот же код, поскольку это не будет учитывать прототип этих функций. Таким образом, сравнение строки кода не гарантирует, что у них один и тот же объект-прототип.
Два объекта должны иметь одинаковую цепочку прототипов, а не только одинаковые свойства. Это можно проверить только в кросс-браузерном режиме, сравнив конструктор обоих объектов на предмет строгого равенства. ECMAScript 5 позволит протестировать их фактический прототип с помощью Object.getPrototypeOf (). Некоторые веб-браузеры также предлагают свойство __proto__, которое делает то же самое. Возможное улучшение приведенного выше кода позволит использовать один из этих методов, когда это возможно.
Использование строгих сравнений здесь имеет первостепенное значение, поскольку 2 не следует считать равным "2,0000", а ложное не следует считать равным нулю, неопределенному или 0.
Соображения эффективности побуждают меня сравнивать на предмет равенства свойств как можно скорее. Тогда, только если это не удалось, ищите typeof этих свойств. Увеличение скорости может быть значительным на больших объектах с большим количеством скалярных свойств.
Не требуется больше двух циклов: первый проверяет свойства из левого объекта, второй проверяет свойства справа и проверяет только существование (не значение), чтобы перехватить эти свойства, которые определены с неопределенным значением.
В целом этот код обрабатывает большинство угловых случаев только в 16 строках кода (без комментариев).
Обновление (13.08.2015). Я реализовал лучшую версию, так как функция value_equals(), которая работает быстрее, правильно обрабатывает угловые случаи, такие как NaN и 0, отличные от -0, опционально предписывая порядок свойств объектов и проверяя циклические ссылки, опираясь на более чем 100 автоматических тестов. как часть тестового пакета проекта Toubkal.
Utils.compareObjects = function(o1, o2){
for(var p in o1){
if(o1.hasOwnProperty(p)){
if(o1[p] !== o2[p]){
return false;
}
}
}
for(var p in o2){
if(o2.hasOwnProperty(p)){
if(o1[p] !== o2[p]){
return false;
}
}
}
return true;
};
Простой способ сравнить только один уровень объектов.
Конечно, это не единственный способ - вы можете создать прототип метода (для Object здесь, но я бы, конечно, не предложил использовать Object для живого кода) для репликации методов сравнения в стиле C#/Java.
Изменить, так как общий пример, как ожидается, ожидается:
Object.prototype.equals = function(x)
{
for(p in this)
{
switch(typeof(this[p]))
{
case 'object':
if (!this[p].equals(x[p])) { return false }; break;
case 'function':
if (typeof(x[p])=='undefined' || (p != 'equals' && this[p].toString() != x[p].toString())) { return false; }; break;
default:
if (this[p] != x[p]) { return false; }
}
}
for(p in x)
{
if(typeof(this[p])=='undefined') {return false;}
}
return true;
}
Обратите внимание, что методы тестирования с помощью toString() абсолютно не достаточно хороши, но метод, который был бы приемлемым, очень труден из-за проблемы наличия или отсутствия пробельного пространства, не говоря уже о методах и методах синонимов, дающих один и тот же результат в разных реализациях. И проблемы прототипирования в отношении объекта в целом.
Следующий алгоритм будет иметь дело со структурами данных, числами, строками, датами и, конечно же, обычными вложенными объектами javascript:
Объекты считаются эквивалентными, когда
- Они в точности равны
===
(Строка и номер распаковываются в первую очередь, чтобы42
эквивалентноNumber(42)
) - или они обе даты и имеют одинаковые
valueOf()
- или они оба одного типа и не нулевые и...
- они не являются объектами и равны
==
(ловит числа / строки / логические значения) - или, игнорируя свойства с
undefined
По значению они имеют одинаковые свойства, все из которых считаются рекурсивно эквивалентными.
- они не являются объектами и равны
Функции не считаются идентичными по тексту функции. Этого теста недостаточно, потому что функции могут иметь разные замыкания. Функции считаются равными, только если ===
говорит так (но вы можете легко расширить это эквивалентное отношение, если вы решите это сделать).
Избегайте бесконечных циклов, потенциально вызванных круговыми структурами данных. когда areEquivalent
пытается опровергнуть равенство и использует для этого свойства объекта, он отслеживает объекты, для которых необходимо это субсравнение. Если равенство может быть опровергнуто, то некоторый путь достижимых свойств отличается между объектами, и тогда должен быть кратчайший такой достижимый путь, и этот кратчайший достижимый путь не может содержать циклы, присутствующие в обоих путях; то есть нормально принимать равенство при рекурсивном сравнении объектов. Предположение хранится в собственности areEquivalent_Eq_91_2_34
, который удаляется после использования, но если граф объектов уже содержит такое свойство, поведение не определено. Использование такого свойства маркера необходимо, потому что javascript не поддерживает словари, использующие произвольные объекты в качестве ключей.
function unwrapStringOrNumber(obj) {
return (obj instanceof Number || obj instanceof String
? obj.valueOf()
: obj);
}
function areEquivalent(a, b) {
a = unwrapStringOrNumber(a);
b = unwrapStringOrNumber(b);
if (a === b) return true; //e.g. a and b both null
if (a === null || b === null || typeof (a) !== typeof (b)) return false;
if (a instanceof Date)
return b instanceof Date && a.valueOf() === b.valueOf();
if (typeof (a) !== "object")
return a == b; //for boolean, number, string, xml
var newA = (a.areEquivalent_Eq_91_2_34 === undefined),
newB = (b.areEquivalent_Eq_91_2_34 === undefined);
try {
if (newA) a.areEquivalent_Eq_91_2_34 = [];
else if (a.areEquivalent_Eq_91_2_34.some(
function (other) { return other === b; })) return true;
if (newB) b.areEquivalent_Eq_91_2_34 = [];
else if (b.areEquivalent_Eq_91_2_34.some(
function (other) { return other === a; })) return true;
a.areEquivalent_Eq_91_2_34.push(b);
b.areEquivalent_Eq_91_2_34.push(a);
var tmp = {};
for (var prop in a)
if(prop != "areEquivalent_Eq_91_2_34")
tmp[prop] = null;
for (var prop in b)
if (prop != "areEquivalent_Eq_91_2_34")
tmp[prop] = null;
for (var prop in tmp)
if (!areEquivalent(a[prop], b[prop]))
return false;
return true;
} finally {
if (newA) delete a.areEquivalent_Eq_91_2_34;
if (newB) delete b.areEquivalent_Eq_91_2_34;
}
}
Я написал этот кусок кода для сравнения объектов, и, похоже, он работает. проверьте утверждения:
function countProps(obj) {
var count = 0;
for (k in obj) {
if (obj.hasOwnProperty(k)) {
count++;
}
}
return count;
};
function objectEquals(v1, v2) {
if (typeof(v1) !== typeof(v2)) {
return false;
}
if (typeof(v1) === "function") {
return v1.toString() === v2.toString();
}
if (v1 instanceof Object && v2 instanceof Object) {
if (countProps(v1) !== countProps(v2)) {
return false;
}
var r = true;
for (k in v1) {
r = objectEquals(v1[k], v2[k]);
if (!r) {
return false;
}
}
return true;
} else {
return v1 === v2;
}
}
assert.isTrue(objectEquals(null,null));
assert.isFalse(objectEquals(null,undefined));
assert.isTrue(objectEquals("hi","hi"));
assert.isTrue(objectEquals(5,5));
assert.isFalse(objectEquals(5,10));
assert.isTrue(objectEquals([],[]));
assert.isTrue(objectEquals([1,2],[1,2]));
assert.isFalse(objectEquals([1,2],[2,1]));
assert.isFalse(objectEquals([1,2],[1,2,3]));
assert.isTrue(objectEquals({},{}));
assert.isTrue(objectEquals({a:1,b:2},{a:1,b:2}));
assert.isTrue(objectEquals({a:1,b:2},{b:2,a:1}));
assert.isFalse(objectEquals({a:1,b:2},{a:1,b:3}));
assert.isTrue(objectEquals({1:{name:"mhc",age:28}, 2:{name:"arb",age:26}},{1:{name:"mhc",age:28}, 2:{name:"arb",age:26}}));
assert.isFalse(objectEquals({1:{name:"mhc",age:28}, 2:{name:"arb",age:26}},{1:{name:"mhc",age:28}, 2:{name:"arb",age:27}}));
assert.isTrue(objectEquals(function(x){return x;},function(x){return x;}));
assert.isFalse(objectEquals(function(x){return x;},function(y){return y+2;}));
Я немного изменил код выше. для меня 0!== ложь и ноль! == не определено. Если вам не нужна такая строгая проверка, удалите один "=" знак "this [p]! == x [p]" внутри кода.
Object.prototype.equals = function(x){
for (var p in this) {
if(typeof(this[p]) !== typeof(x[p])) return false;
if((this[p]===null) !== (x[p]===null)) return false;
switch (typeof(this[p])) {
case 'undefined':
if (typeof(x[p]) != 'undefined') return false;
break;
case 'object':
if(this[p]!==null && x[p]!==null && (this[p].constructor.toString() !== x[p].constructor.toString() || !this[p].equals(x[p]))) return false;
break;
case 'function':
if (p != 'equals' && this[p].toString() != x[p].toString()) return false;
break;
default:
if (this[p] !== x[p]) return false;
}
}
return true;
}
Затем я проверил это со следующими объектами:
var a = {a: 'text', b:[0,1]};
var b = {a: 'text', b:[0,1]};
var c = {a: 'text', b: 0};
var d = {a: 'text', b: false};
var e = {a: 'text', b:[1,0]};
var f = {a: 'text', b:[1,0], f: function(){ this.f = this.b; }};
var g = {a: 'text', b:[1,0], f: function(){ this.f = this.b; }};
var h = {a: 'text', b:[1,0], f: function(){ this.a = this.b; }};
var i = {
a: 'text',
c: {
b: [1, 0],
f: function(){
this.a = this.b;
}
}
};
var j = {
a: 'text',
c: {
b: [1, 0],
f: function(){
this.a = this.b;
}
}
};
var k = {a: 'text', b: null};
var l = {a: 'text', b: undefined};
a == b ожидаемая истина; вернул истину
a == c ожидается ложь; вернулся ложный
c == d ожидал ложного; вернулся ложный
a == ожидаемое ложное; вернулся ложный
f==g ожидаемая истина; вернул истину
h==g ожидается ложь; вернулся ложный
я ==j ожидал истины; вернул истину
d==k ожидается ложь; вернулся ложный
k== я ожидал ложного; вернулся ложный
Если вы хотите явно проверить методы, вы можете использовать методы method.toSource() или method.toString().
Вот моя версия, многое из этого потока интегрировано (то же самое для тестовых случаев):
Object.defineProperty(Object.prototype, "equals", {
enumerable: false,
value: function (obj) {
var p;
if (this === obj) {
return true;
}
// some checks for native types first
// function and sring
if (typeof(this) === "function" || typeof(this) === "string" || this instanceof String) {
return this.toString() === obj.toString();
}
// number
if (this instanceof Number || typeof(this) === "number") {
if (obj instanceof Number || typeof(obj) === "number") {
return this.valueOf() === obj.valueOf();
}
return false;
}
// null.equals(null) and undefined.equals(undefined) do not inherit from the
// Object.prototype so we can return false when they are passed as obj
if (typeof(this) !== typeof(obj) || obj === null || typeof(obj) === "undefined") {
return false;
}
function sort (o) {
var result = {};
if (typeof o !== "object") {
return o;
}
Object.keys(o).sort().forEach(function (key) {
result[key] = sort(o[key]);
});
return result;
}
if (typeof(this) === "object") {
if (Array.isArray(this)) { // check on arrays
return JSON.stringify(this) === JSON.stringify(obj);
} else { // anyway objects
for (p in this) {
if (typeof(this[p]) !== typeof(obj[p])) {
return false;
}
if ((this[p] === null) !== (obj[p] === null)) {
return false;
}
switch (typeof(this[p])) {
case 'undefined':
if (typeof(obj[p]) !== 'undefined') {
return false;
}
break;
case 'object':
if (this[p] !== null
&& obj[p] !== null
&& (this[p].constructor.toString() !== obj[p].constructor.toString()
|| !this[p].equals(obj[p]))) {
return false;
}
break;
case 'function':
if (this[p].toString() !== obj[p].toString()) {
return false;
}
break;
default:
if (this[p] !== obj[p]) {
return false;
}
}
};
}
}
// at least check them with JSON
return JSON.stringify(sort(this)) === JSON.stringify(sort(obj));
}
});
Вот мой TestCase:
assertFalse({}.equals(null));
assertFalse({}.equals(undefined));
assertTrue("String", "hi".equals("hi"));
assertTrue("Number", new Number(5).equals(5));
assertFalse("Number", new Number(5).equals(10));
assertFalse("Number+String", new Number(1).equals("1"));
assertTrue([].equals([]));
assertTrue([1,2].equals([1,2]));
assertFalse([1,2].equals([2,1]));
assertFalse([1,2].equals([1,2,3]));
assertTrue(new Date("2011-03-31").equals(new Date("2011-03-31")));
assertFalse(new Date("2011-03-31").equals(new Date("1970-01-01")));
assertTrue({}.equals({}));
assertTrue({a:1,b:2}.equals({a:1,b:2}));
assertTrue({a:1,b:2}.equals({b:2,a:1}));
assertFalse({a:1,b:2}.equals({a:1,b:3}));
assertTrue({1:{name:"mhc",age:28}, 2:{name:"arb",age:26}}.equals({1:{name:"mhc",age:28}, 2:{name:"arb",age:26}}));
assertFalse({1:{name:"mhc",age:28}, 2:{name:"arb",age:26}}.equals({1:{name:"mhc",age:28}, 2:{name:"arb",age:27}}));
assertTrue("Function", (function(x){return x;}).equals(function(x){return x;}));
assertFalse("Function", (function(x){return x;}).equals(function(y){return y+2;}));
var a = {a: 'text', b:[0,1]};
var b = {a: 'text', b:[0,1]};
var c = {a: 'text', b: 0};
var d = {a: 'text', b: false};
var e = {a: 'text', b:[1,0]};
var f = {a: 'text', b:[1,0], f: function(){ this.f = this.b; }};
var g = {a: 'text', b:[1,0], f: function(){ this.f = this.b; }};
var h = {a: 'text', b:[1,0], f: function(){ this.a = this.b; }};
var i = {
a: 'text',
c: {
b: [1, 0],
f: function(){
this.a = this.b;
}
}
};
var j = {
a: 'text',
c: {
b: [1, 0],
f: function(){
this.a = this.b;
}
}
};
var k = {a: 'text', b: null};
var l = {a: 'text', b: undefined};
assertTrue(a.equals(b));
assertFalse(a.equals(c));
assertFalse(c.equals(d));
assertFalse(a.equals(e));
assertTrue(f.equals(g));
assertFalse(h.equals(g));
assertTrue(i.equals(j));
assertFalse(d.equals(k));
assertFalse(k.equals(l));
Если вы работаете без библиотеки JSON, возможно, это поможет вам:
Object.prototype.equals = function(b) {
var a = this;
for(i in a) {
if(typeof b[i] == 'undefined') {
return false;
}
if(typeof b[i] == 'object') {
if(!b[i].equals(a[i])) {
return false;
}
}
if(b[i] != a[i]) {
return false;
}
}
for(i in b) {
if(typeof a[i] == 'undefined') {
return false;
}
if(typeof a[i] == 'object') {
if(!a[i].equals(b[i])) {
return false;
}
}
if(a[i] != b[i]) {
return false;
}
}
return true;
}
var a = {foo:'bar', bar: {blub:'bla'}};
var b = {foo:'bar', bar: {blub:'blob'}};
alert(a.equals(b)); // alert's a false