Avatar ·

Сравнение объектов в JavaScript: почему {} !== {} и как правильно сравнивать?

Всем привет! Я только начал изучать JavaScript и столкнулся с непонятной ситуацией. Я пытаюсь сравнить два объекта, которые выглядят совершенно одинаково, но получаю false. Вот мой код: ```javascript const user1 = { name: 'Анна', age: 25 }; const user2 = { name: 'Анна', age: 25 }; console.log(user1 === user2); // false console.log(user1 == user2); // тоже false ``` Я ожидал, что будет true, ведь свойства и их значения совпадают! Но JavaScript говорит, что они разные. Почему так происходит? Я понимаю, что для чисел и строк это работает нормально: ```javascript console.log(5 === 5); // true console.log('привет' === 'привет'); // true ``` А с объектами — нет. Я пробовал сравнивать через JSON.stringify: ```javascript console.log(JSON.stringify(user1) === JSON.stringify(user2)); // true ``` Но мне сказали, что это плохой способ, особенно если объекты содержат функции или даты. Подскажите, как правильно сравнивать объекты по содержимому? Есть ли встроенные методы для глубокого сравнения? И почему вообще JavaScript так странно себя ведет с объектами? Заранее спасибо за помощь! <<>>

Почему объекты сравниваются по ссылке, а не по значению?

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

const obj1 = { a: 1 };
const obj2 = { a: 1 };
const obj3 = obj1; // копируем ссылку

console.log(obj1 === obj2); // false — разные объекты
console.log(obj1 === obj3); // true — один и тот же объект

Как правильно сравнивать объекты по содержимому?

Есть несколько подходов. Рассмотрим их от простого к сложному.

1. Поверхностное сравнение (shallow compare)

Подходит, если объект имеет только примитивные свойства (строки, числа) и не имеет вложенных объектов:

function shallowEqual(obj1, obj2) {
  const keys1 = Object.keys(obj1);
  const keys2 = Object.keys(obj2);

  if (keys1.length !== keys2.length) return false;

  for (let key of keys1) {
    if (obj1[key] !== obj2[key]) return false;
  }

  return true;
}

const a = { x: 1, y: 2 };
const b = { x: 1, y: 2 };
console.log(shallowEqual(a, b)); // true

2. Глубокое сравнение с помощью JSON.stringify (с ограничениями)

Самый простой, но ненадежный способ:

const obj1 = { name: 'Анна', date: new Date(2023, 0, 1) };
const obj2 = { name: 'Анна', date: new Date(2023, 0, 1) };

console.log(JSON.stringify(obj1) === JSON.stringify(obj2)); // false! Потому что Date преобразуется в строку по-разному

Недостатки:

  • Не работает для функций, undefined, Symbol, Infinity, NaN
  • Чувствителен к порядку ключей (хотя в современных движках порядок обычно сохраняется)
  • Медленный для больших объектов

3. Рекурсивное глубокое сравнение

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

function deepEqual(obj1, obj2) {
  // Проверка на примитивы и одинаковые ссылки
  if (obj1 === obj2) return true;

  // Если хотя бы один не объект или null
  if (typeof obj1 !== 'object' || obj1 === null ||
      typeof obj2 !== 'object' || obj2 === null) {
    return false;
  }

  // Специальная обработка для Date
  if (obj1 instanceof Date && obj2 instanceof Date) {
    return obj1.getTime() === obj2.getTime();
  }

  // Специальная обработка для RegExp
  if (obj1 instanceof RegExp && obj2 instanceof RegExp) {
    return obj1.toString() === obj2.toString();
  }

  const keys1 = Object.keys(obj1);
  const keys2 = Object.keys(obj2);

  if (keys1.length !== keys2.length) return false;

  for (let key of keys1) {
    if (!keys2.includes(key)) return false;
    if (!deepEqual(obj1[key], obj2[key])) return false;
  }

  return true;
}

// Пример использования
const user1 = { 
  name: 'Анна', 
  age: 25,
  address: { city: 'Москва', street: 'Тверская' }
};
const user2 = { 
  name: 'Анна', 
  age: 25,
  address: { city: 'Москва', street: 'Тверская' }
};

console.log(deepEqual(user1, user2)); // true

4. Использование готовых библиотек

В реальных проектах лучше не писать свои велосипеды, а использовать проверенные решения:

  • Lodash — _.isEqual(obj1, obj2) — самая популярная утилита
  • Ramda — R.equals(obj1, obj2)
  • Fast-deep-equal — легковесная библиотека
// Установка: npm install lodash
const _ = require('lodash');

const obj1 = { a: { b: 2 } };
const obj2 = { a: { b: 2 } };
console.log(_.isEqual(obj1, obj2)); // true

Когда нужно сравнивать объекты?

Чаще всего это требуется в React/Vue для оптимизации ре-рендеров (React.memo, useMemo), в тестах (Jest — toEqual), или при проверке изменений состояния в Redux.

Запомните главное: операторы сравнения работают с объектами как со ссылками. Для сравнения по содержимому всегда используйте специальные функции.

<<>>

Альтернативный подход: сравнение через Map и Set

Иногда нужно сравнивать не просто объекты, а их структуру или наличие определенных ключей. В этом случае удобно использовать коллекции Map и Set.

Сравнение набора ключей

function hasSameKeys(obj1, obj2) {
  const keys1 = new Set(Object.keys(obj1));
  const keys2 = new Set(Object.keys(obj2));
  
  if (keys1.size !== keys2.size) return false;
  
  for (let key of keys1) {
    if (!keys2.has(key)) return false;
  }
  return true;
}

const a = { name: 'Иван', age: 30 };
const b = { name: 'Петр', age: 25 };
console.log(hasSameKeys(a, b)); // true — одинаковый набор ключей

Сравнение с игнорированием порядка свойств

Если вам не важен порядок ключей (хотя в ES2015+ он гарантированно сохраняется), можно использовать сортировку:

function sortedStringify(obj) {
  const sortedKeys = Object.keys(obj).sort();
  const sortedObj = {};
  for (let key of sortedKeys) {
    sortedObj[key] = obj[key];
  }
  return JSON.stringify(sortedObj);
}

const x = { b: 2, a: 1 };
const y = { a: 1, b: 2 };
console.log(sortedStringify(x) === sortedStringify(y)); // true

Когда это полезно?

  • При сравнении конфигурационных объектов
  • В тестах, где важен только набор полей, а не их значения
  • При работе с формами и проверке изменений

Помните: для сложных вложенных структур всегда лучше использовать _.isEqual из Lodash — это сэкономит вам часы отладки и покрывает 99% edge-кейсов.