Сравнение объектов в JavaScript
Заголовок: Как правильно сравнивать объекты в JavaScript?
Я сталкиваюсь с проблемой сравнения объектов в JavaScript. Как известно, в JavaScript два объекта равны только в том случае, если они ссылаются на один и тот же экземпляр объекта. Например, в следующем коде:
var user1 = {name: "nerd", org: "dev"};
var user2 = {name: "nerd", org: "dev"};
var eq = user1 == user2;
alert(eq); // выводит false
Результат сравнения false
, хотя значения атрибутов объектов user1
и user2
идентичны.
Я понимаю, что два объекта равны только если ссылаются на один и тот же объект, но есть ли способ проверить, имеют ли они одинаковые значения атрибутов?
Я пробовал следующий метод, который работает для меня, но является ли это единственным возможным решением?
var eq = Object.toJSON(user1) == Object.toJSON(user2);
alert(eq); // выводит true
Буду признателен за любые советы или альтернативные подходы к решению данной проблемы!
5 ответ(ов)
Вы можете использовать следующий код для сравнения двух объектов, но имейте в виду, что он сравнивает только объекты одного уровня (то есть, он не учитывает вложенные объекты):
Utils.compareObjects = function(o1, o2) {
for (var p in o1) {
if (o1.hasOwnProperty(p)) {
if (o1[p] !== o2[p]) {
return false; // Если значения не равны, возвращаем false
}
}
}
for (var p in o2) {
if (o2.hasOwnProperty(p)) {
if (o1[p] !== o2[p]) {
return false; // Повторно проверяем значения из второго объекта
}
}
}
return true; // Если все значения равны, возвращаем true
};
Этот метод проходит по свойствам обоих объектов, сравнивая их значения. Если значения разных свойств отличаются, функция возвращает false
. Только если все свойства равны, вернется true
. Учтите, что данный код работает только для простых объектов без вложенных структур.
Конечно, это не единственный способ — вы можете создать метод (здесь против Object, но я определенно не рекомендую использовать Object в рабочем коде), чтобы воспроизвести методы сравнения в стиле C#/Java.
Вот общий пример, который может соответствовать вашим ожиданиям:
Object.prototype.equals = function(x) {
for (let 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 (let p in x) {
if (typeof(this[p]) == 'undefined') { return false; }
}
return true;
}
Имейте в виду, что тестирование методов с помощью toString() абсолютно не является хорошим решением, но создать метод, который был бы приемлемым, очень сложно из-за проблемы с пробелами, которые могут иметь значение или нет, не говоря уже о синонимичных методах и методах, производящих одинаковый результат при разных реализациях. И проблемы прототипирования против Object в целом.
Разделим алгоритм на части и постараемся структурировать его ответ на русском языке в стиле StackOverflow.
Вопрос: Как сравнивать сложные вложенные объекты в JavaScript на эквивалентность?
Ответ:
Для сравнения сложных вложенных объектов, строк, дат и чисел в JavaScript можно использовать следующий алгоритм. Данный алгоритм позволяет избежать бесконечных циклов, которые могут возникать в следствии наличия циклических структур данных.
Условия эквивалентности
Два объекта считаются эквивалентными, если:
- Они равны по
===
(при этом строки и числа предварительно "разворачиваются" для проверки равенства, например,42
эквивалентноNumber(42)
). - Оба объекта являются датами, и их значения, полученные методами
valueOf()
, равны. - Оба объекта имеют одинаковый тип, не равны
null
и удовлетворяют одному из следующих условий:- Они не являются объектами и равны по
==
(это захватывает числа, строки и булевы значения). - Игнорируя свойства со значением
undefined
, они имеют те же свойства, которые считаются рекурсивно эквивалентными.
- Они не являются объектами и равны по
Функции не принимаются в расчет для проверки идентичности по тексту функции, так как они могут иметь разные замыкания. Функции считаются равными только если это подтверждено с помощью ===
.
Избежание бесконечных циклов
При попытке проверить равенство, алгоритм отслеживает объекты, для которых требуется выполнить подкомпарацию, чтобы избежать бесконечных циклов в случае циклических структур данных. Если можно опровергнуть равенство, значит, существует путь к различающеся свойству, и алгоритм сохраняет это предположение в свойстве areEquivalent_Eq_91_2_34
. После использования это свойство удаляется.
Пример кода
Вот пример реализации алгоритма:
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; // Например, оба 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; // Для булевых значений, чисел, строк
const 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(other => other === b)) return true;
if (newB) b.areEquivalent_Eq_91_2_34 = [];
else if (b.areEquivalent_Eq_91_2_34.some(other => other === a)) return true;
a.areEquivalent_Eq_91_2_34.push(b);
b.areEquivalent_Eq_91_2_34.push(a);
const tmp = {};
for (const prop in a)
if(prop != "areEquivalent_Eq_91_2_34")
tmp[prop] = null;
for (const prop in b)
if (prop != "areEquivalent_Eq_91_2_34")
tmp[prop] = null;
for (const 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;
}
}
Заключение
Данный метод позволяет эффективно сравнивать объекты с учетом различных типов и позволяет избежать проблемы бесконечной рекурсии. Вы можете использовать этот алгоритм для сравнения сложных данных в ваших JavaScript проектах.
Ваш код для сравнения объектов выглядит хорошо и в целом работает корректно, что подтверждается вашими ассерциями. Однако есть несколько моментов, которые стоит обсудить.
Перебор свойств объектов: Вы используете цикл
for...in
, что является допустимым, но рекомендуется дополнительно использовать методObject.keys()
для получения массива ключей, а затем итерироваться по этому массиву. Это поможет избежать нежелательных результатов, если объект унаследует свойства от прототипа.Сравнение функций: Ваша логика сравнения функций по строковому представлению (
toString
) может не сработать в некоторых случаях, особенно если функции имеют разные области видимости или замыкания. Например, две разные функции с одинаковым кодом могут иметь разные ссылки, что приведет к ошибочному результату.Сравнение массивов: Вы сравниваете массивы в вашем коде, но не учитываете порядок элементов, что может вызвать путаницу. Убедитесь, что порядок важен, когда вы сравниваете массивы.
Сравнение вложенных объектов: Ваш код не обрабатывает случаи, когда в объектах/массивах могут быть циклические ссылки, что может привести к бесконечным циклам и переполнению стека.
В итоге, ваш код действительно выполняет свою задачу, подтвержденную ассерциями, но для повышения его надежности и универсальности стоит рассмотреть возможность применения вышеупомянутых улучшений.
Вот пример возможного улучшения функции countProps
с использованием Object.keys()
:
function countProps(obj) {
return Object.keys(obj).length;
}
Желаю успехов в дальнейшей разработке! Если у вас есть дополнительные вопросы, не стесняйтесь их задавать.
Для проверки равенства объектов у вас есть хороший метод, который вы немного модифицировали. Ваша логика, что *0 !== false*
и *null !== undefined*
, полностью оправдана. Если вам не требуется столь строгая проверка, вы можете убрать один знак равенства в строке *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
ожидаетсяtrue
; возвращеноtrue
a==c
ожидаетсяfalse
; возвращеноfalse
c==d
ожидаетсяfalse
; возвращеноfalse
a==e
ожидаетсяfalse
; возвращеноfalse
f==g
ожидаетсяtrue
; возвращеноtrue
h==g
ожидаетсяfalse
; возвращеноfalse
i==j
ожидаетсяtrue
; возвращеноtrue
d==k
ожидаетсяfalse
; возвращеноfalse
k==l
ожидаетсяfalse
; возвращеноfalse
Ваш код даёт корректные результаты для всех тестов. Если вам нужны менее строгие сравнения, не забудьте внести соответствующие изменения. Отличная работа!
Преобразование объекта JS в строку JSON
Как проверить наличие ключа в объекте JavaScript?
Как удалить все дубликаты из массива объектов?
Как получить подмножество свойств объекта JavaScript?
Какова разница между `throw new Error` и `throw someObject`?