Более глубокое сравнение объектов в Javascript

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

Если вы хотите следовать коду, рассмотрите возможность использования REPL.it Javascript console.

Проблема

Давайте рассмотрим приведенный ниже код:

var harry = {
    school: "Hogwarts",
    occupation: "Wizard in training"
};
var hermione = {
    school: "Hogwarts",
    occupation: "Wizard in training"
};

Мы можем взглянуть на эти две переменные и довольно быстро решить, что они действительно равны; они оба являются объектами с точно совпадающими парами ключ-значение. Javascript обязательно должен согласиться, верно?

Вот что произойдет, если мы введем приведенный ниже код в нашем Node REPL:

harry === hermione
> false
harry == hermione
>false

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

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

Так что же на самом деле сравнивается, когда мы запускаем приведенный выше код? Ответ кроется в этой заметке о равенстве от MDNЕсли оба операнда являются объектами, то JavaScript сравнивает внутренние ссылки, которые равны, когда операнды ссылаются на один и тот же объект в памяти.

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

"Если оба операнда являются объектами, то JavaScript сравнивает внутренние ссылки, которые равны, когда операнды ссылаются на один и тот же объект в памяти".

Обратите внимание на приведенный ниже пример для доказательства. Сначала мы создадим точную копию harry, а затем сравним ее с нашей исходной переменной harry.

var cloneOfHarry = harry;
cloneOfHarry;
> { school: 'Hogwarts', occupation: 'Wizard in training' }
harry === cloneOfHarry;
> true

Поскольку ранее мы ввели var cloneOfHarry = harry, новая переменная указывает на то же место в памяти, что и исходная переменная harry. Таким образом, оператор равенства возвращает true.

Чтобы действительно донести мысль, далее мы изменим cloneOfHarry, удалив ключ занятия, чтобы он больше не был структурно похож на harry. Исходная переменная harry останется нетронутой (на первый взгляд).

delete cloneOfHarry.occupation;
cloneOfHarry;
> { school: 'Hogwarts' }
harry;
> { school: 'Hogwarts' }
harry === cloneOfHarry;
>true

Мы видим, что со структурной точки зрения harry и cloneOfHarry все те же. Когда мы удаляем ключ occupation из cloneOfHarry, мы изменяем то, что хранится в этом месте памяти, что повлияет на любую переменную, указывающую на это место. Оператор равенства продолжает возвращать true, поскольку ни одна из переменных не претерпела изменения в позиции памяти.

Решение

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

Ниже приведено очень простое решение, которое я написал:

Основная идея состоит в том, чтобы перебрать каждую пару ключ-значение в objectA и сравнить их с соответствующей парой ключ-значение в objectB. Если ключ не существует в objectB, undefined будет возвращено, когда мы попытаемся получить доступ к этому ключу, что завершит цикл, в результате чего наша функция objectsAreEqual немедленно вернет false. Аналогичным образом, если ключ существует в обоих объектах, но с разными значениями, цикл будет прерван, и функция objectsAreEqual немедленно вернет false. Если мы успешно пройдем через цикл, мы можем разумно предположить, что объекты равны, и objectsAreEqual вернет true.

Эта функция будет работать в простых случаях, но в этой итерации есть как минимум одна вопиющая проблема. В случае, если objectB имеет больше ключей, чем objectA, функция все равно вернет значение true. К счастью, это можно довольно легко исправить. Кроме того, циклы могут быть потенциально дорогостоящими операциями, поэтому мы должны стараться не зацикливаться, если только некоторые более простые методы установления равенства не помогают.

Ниже приведена немного более сложная версия, которая обрабатывает проблемы, которые мы диагностировали в нашей предыдущей попытке:

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

Проведя быстрый тест, мы должны увидеть следующий результат:

objectsAreEqual(harry, hermione);
> true

Последние мысли

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

Примечание. Обе библиотеки Underscore и LoDash включают метод с именем _.isEqual(), который выполняет сравнение объектов, аналогичное созданному нами. Взглянуть на их исходный код, чтобы увидеть, как они подошли к проблеме, и предложено, и весело.