Я читал грамматику javascript автора JavaScript Teacher, когда вдруг наткнулся на сравнение объектов в одной из глав. Несколько лет назад, когда я все еще изучал основы javascript, мне было трудно понять примитивные типы данных и объекты. Мне просто было непонятно, почему операторы сравнения не работают с объектами.

Чтение об этой же проблеме в грамматике javascript напомнило мне об этой проблеме и вдохновило меня написать базовый шаблонный код, который помогает сравнивать как простые, так и сложные объекты с помощью рекурсивной функции.

Проект полностью с открытым исходным кодом, и его можно найти здесь в моем репозитории на github. Он также был опубликован как модуль npm, и его можно найти здесь.



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

Все в JavaScript может быть представлено как объект, включая его примитивные типы данных. Когда мы хотим проверить равенство между двумя типами данных, мы просто используем операторы сравнения для этих двух типов данных.

let a = 1
let b = 1
console.log(a === b) //returns true for a primitive value
let objA = {}
let objB = {}
let objC = objA
console.log(objA === objB) //returns false
console.log(ObjA === objC) //returns true

Как показано выше, сравнение двух разных чисел с одним и тем же значением будет оценено как true, как и ожидалось, но почему сравнение двух объектов возвращает false в первом случае и true в последнем случае, даже если они явно одинаковы?

Данные примитивов передаются по значению, что означает передачу копии самого примитива, в то время как объекты передаются по их ссылке. Таким образом, один и тот же объект, хранящийся в двух разных переменных, имеет разные ссылки и, следовательно, оценивается как false. Так как же нам сравнить два объекта, даже если они имеют совершенно разные ссылки?

Очень простым подходом к этому было бы преобразование объектов в строки с помощью JSON.stringify(object) и сравнение строк друг с другом, но тогда возникает другая проблема.

let a = {
    name: 'felix',
    age: 30,
    weight: '45kg'
}
let b = {
    name: 'felix',
    age: 30,
    weight: '45kg'
}
let c = {
    age: 30,
    weight: '45kg',
    name: 'felix'
}
console.log(JSON.stringify(a) === jSON.stringify(b)) //returns true
console.log(JSON.stringify(a) === JSON.stringify(c)) //returns false

В приведенном выше примере кода мы заметим, что переменные a, b and c являются точно такими же объектами, хотя c структурированы иначе, чем остальные, но по-прежнему имеют ту же самую пару ключ-значение, что и остальные. Но поскольку c был переупорядочен, он оценивается как false. Даже если он не переупорядочен, он все равно будет оцениваться как false, если ваш объект содержит нодлисты и htmlколлекции.

Итак, как нам это сделать? Давайте сначала напишем простую функцию, которая принимает два объекта в качестве аргумента. Функция должна возвращать true, если эти объекты равны, и выдавать ошибку, когда что-то еще идет не так.

function compareObjects(obj1, obj2){
    return true;
}

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

function reject(msg) {
    throw new Error(msg);
}

Итак, прежде чем мы начнем что-то делать, мы хотели бы убедиться, что мы действительно работаем с двумя объектами, верно? Итак, давайте напишем простой скрипт, который сделает это за нас!

function compareObjects(obj1, obj2){
//first confirm if both parameters passed to the arguments are objects 
if (typeof obj1 !== 'object' || typeof obj2 !== 'object') {
reject("Parameters passed into the arguments must be Objects")
}
return true;
}

Хорошо, теперь мы можем убедиться, что два переданных параметра являются объектами, но что, если они оба созданы из другого конструктора? Что, если бы один был объектом Date, а другой — массивом? Они оба являются неподвижными объектами и должны пройти первоначальную проверку, верно? Мы хотим убедиться, что только объекты одного и того же конструктора переходят к следующему этапу. мы могли бы сделать это как таковое

//Check if the Objects have different constructors
if (obj1.constructor !== obj2.constructor) {
    reject("Objects have different constructors!");
}

Здорово! теперь мы уверены, что оба параметра, переданные в аргументы, являются объектами и принадлежат одним и тем же конструкторам.

Следующее, что мы хотели бы сделать, это получить все ключи обоих объектов в виде массива, чтобы мы могли убедиться, что каждый из этих объектов имеет одинаковые ключи и значения.

let objKeys1 = Object.keys(obj1).sort(); 
let objKeys2 = Object.keys(obj2).sort();

Вы сразу заметите, что ключи обоих объектов были отсортированы с помощью Array.prototype.sort method.. Это сделано для обеспечения единообразия между массивами ключей обоих объектов, чтобы они были расположены в одном порядке. Прежде чем продолжить, мы хотели бы подтвердить, что оба объекта имеют одинаковое количество ключей, верно?

//If the keys are of different lengths 
if (objKeys1.length !== objKeys2.length) {
    reject("Ooops!, the objects passed in are structured differently")
}

Так как теперь мы уверены, что каждый объект имеет одинаковое количество ключей, мы хотели бы сделать пару вещей.

  1. Переберите каждый объект и проверьте, равны ли все эти ключи
  2. Если это так, мы хотели бы проверить тип каждого значения объекта и сравнить их друг с другом.
for (let i in objKeys1) {
//check if both object have the same keys arranged in the same order
    if (objKeys1[i] !== objKeys2[i]) {
        reject("Ooops!, the objects passed in are structured     differently. Their keys mismatched!")
}else{
        //handle each type of supported values differently
        const value2 = obj2[objKeys1[i]];
        const value1 = obj1[objKeys1[i]]; 
        // handle different types of objects
    }
}

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

Функция valueHandler() проверяет тип переданного объекта с помощью оператора switch и соответствующим образом обрабатывает сравнение. Сравнение довольно прямолинейно для логических чисел и строк.

Для функций он вызывает обе функции и сравнивает возвращаемые ими значения, поскольку нет возможности действительно проверить, что происходит внутри функции.

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

Чтобы точно знать, с каким типом объекта мы имеем дело, мы сначала проверяем instanceof объект. Если экземпляр является обычным выражением, мы просто конвертируем оба объекта в строку и сравниваем их.

Если это дата, мы просто сравниваем .valueOf объектов этой даты, в то время как для наборов мы проверяем, имеют ли они одинаковый размер, и проверяем, имеет ли каждый элемент в первом наборе тот же самый элемент во втором наборе.

Все становится немного интереснее, когда значение представляет собой массив. Массив может содержать любой тип объекта. Он может содержать другой массив, литерал объекта, дату, регулярное выражение, логическое значение, строку, число, наборы и т. д.

Так как же нам обрабатывать все эти данные? Изобретая велосипед! мы просто снова вызываем функцию valueHandler(). Функция, вызывающая сама себя? О да, рекурсия!! Функция valueHandler() обрабатывает как примитивные типы данных, так и объекты и, следовательно, может использоваться для сравнения объектов любого типа в массивах.

Что, если значение является литералом объекта? изначально мы написали функцию compareObjects() для объектных литералов, но теперь у нас есть объектный литерал внутри объектного литерала, что просто означает, что мы можем вызывать compareObjects() рекурсивно.

Вся кодовая база доступна здесь на github.

Тестирование кода

Проект опубликован как пакет npm.

Установка

npm install compare-object --save

После установки модуля вы можете потребовать его, как показано ниже.

const objCompare = require('compare-object'); 

Если у нас есть базовый объект a и be, который содержит несколько типов данных для сравнения, как показано

const a = {
a: 1,
b: 'string',
c: true,
d: () => 'wooow!!',
e: new Date(15, 7, 2019),
f: new RegExp('Gbadebo', 'gi'),
g: new Set([1, 2, 3, 4]),h: [1, 2, 3, true, "gbadebo", new Date(15, 7, 2019), new Set([4, 5, 6, 7])]
const b = {
f: new RegExp('Gbadebo', 'gi'),
c: true,e: new Date(15, 7, 2019),
h: [1, 2, 3, true, "gbadebo", new Date(15, 7, 2019), new Set([4, 5, 6, 7])],
b: 'string'
g: new Set([1, 2, 3, 4]),d: () => 'wooow!!',
a: 1}
console.log(objCompare(a, b)) //returns true

Мы бы сразу заметили это, хотя объект b был переустроен. Он почти такой же, как объект a.

Давайте попробуем это с более сложным объектом и заметим, что он все еще работает достаточно хорошо.

если мы запустим objCompare(obj1, obj2), он вернет true, как и ожидалось, поскольку и obj1, и obj2 одинаковы, но имеют разную структуру.

Заключение

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

Спасибо за чтение!

Ваше здоровье!!!