Задача: создать функцию глубокого равенства для проверки двух javascript-объектов друг против друга и выяснить, являются ли они одинаковыми. Конечно, простое тройное равенство не поможет, поскольку Javascript сравнивает объекты по ссылке на места двух объектов в памяти — независимо от того, идентичны ли пары ключ/значение.

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

Этот первый шаг так же прост, как:

  if(a === b) {
    return 'These variables reference the same object';
  }

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

  if(Object.keys(a).length !== Object.keys(b).length)
    return false;

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

Самым простым способом будет следующий:

for(let key in a) {
      for(let key in b) {
        if(a[key] instanceof Object && b[key] instanceof Object){
          return deepEquals(a[key], b[key]);
        }
        if(a[key] !== b[key]){
          return false;
        }
        if(key !== key) {
          return false;
        }
      }
    }
return true;

Вот идеально подходящая проверка для двух объектов, которые, как мы знаем, не преподнесут сюрпризов. Если значение каждого свойства каждого объекта является примитивным типом данных, эта функция будет работать нормально. Мы начинаем с двух циклов for-in для сравнения объектов. Наша первая проверка состоит в том, чтобы увидеть, являются ли эти два объекта еще одним вложенным объектом, и если да, мы возвращаем значение повторного вызова нашей функции. Если нет, мы можем просто сравнить ключи и сравнить значения.

Итак, мы закончили. Не взглядом. Что, если текущее значение, которое мы итерируем, является массивом? Наша проверка instanceof не будет учитывать это. Итак, давайте добавим более полные проверки к нашему первому потенциальному использованию рекурсии:

if(a[key] instanceof Object &&
   b[key] instanceof Object &&
   !Array.isArray(a[key]) &&
   !Array.isArray(b[key]))

Это может показаться безумием для одного оператора if, но он проверяет все, что нам нужно: является ли каждое значение объектом? Каждое значение не является массивом? Хорошо пойти.

…Но что, если это массив?! Аналогично с массивами, глубокое сравнение Javascript не возвращает «истину» для массивов с одинаковыми значениями по той же причине, что и для объектов: массивы — это объекты, и даже с одинаковыми значениями они ссылаются на разные места в памяти. Итак, как проще всего их сравнить… почему, переходя от эталонного к значению, конечно. Быстрый .join(‘’) превращает наши массивы в сопоставимые строки, даже если массивы содержат вложенные объекты, дополнительные массивы или что-то еще.

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

else if(Array.isArray(a[key]) && Array.isArray(b[key])) {
          const arr1 = a[key];
          const arr2 = b[key];
          if(arr1.join('') === arr2.join('')) {
            continue;
          }
        }

Если массивы идентичны по значению, мы идем дальше и не бросаем «ложное» логическое значение. Как только мы это сделали, все, что осталось, — это простое сравнение ключей и значений.

if(a[key] !== b[key]){
          return false;
        }
        if(key !== key) {
          return false;
        }

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

const deepEquals = (a, b) => {
  if(Object.keys(a).length !== Object.keys(b).length)
    return false;
    for(let key in a) {
      for(let key in b) {
        if(a[key] instanceof Object &&
          b[key] instanceof Object &&
          !Array.isArray(a[key]) &&
          !Array.isArray(b[key])) {
            return deepEquals(a[key], b[key]);
         } else if(Array.isArray(a[key]) && Array.isArray(b[key])) {
          const arr1 = a[key];
          const arr2 = b[key];
          if(arr1.join('') === arr2.join('')) {
            continue;
          }
        }
        if(a[key] !== b[key]){
          return false;
        }
        if(key !== key) {
          return false;
        }
      }
    }
  return true;
}

Давайте проверим это:

let one = { foo: 1, b: { c: { d: { e: 'potato' } } } };
let two = { foo: 1, b: { c: { d: { e: 'potato' } } } };
deepEquals(one, two) // returns 'true'.
let one = { foo: 1, b: [1, 4, 2, 3, [1, 2, { a: 'name' }]] };
let two = { foo: 1, b: [1, 2, 3, [1, 2, { a: 'name' }]] };
deepEquals(one, two) // returns 'false'.
Now for a real challenge...
let one = { foo: 1, b: [1, 2, 3, [1, 2, { a: 'name' }]] };
let two = { foo: 1, b: [1, 2, 3, [1, 2, { a: 'name' }]] };
deepEquals(one, two) // returns 'true'. Nice!