Сравнение объектов в 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
Ваш код даёт корректные результаты для всех тестов. Если вам нужны менее строгие сравнения, не забудьте внести соответствующие изменения. Отличная работа!
Как удалить свойство из объекта JavaScript?
Преобразование объекта JS в строку JSON
Как получить доступ к вложенным объектам, массивам или JSON и обработать их?
Как удалить все дубликаты из массива объектов?
Почему null является объектом и в чем разница между null и undefined?