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

Например:

const obj1 = {
  name: "Dillion"
}
const obj2 = {
  name: "Dillion"
}

console.log(obj1 === obj2)
// false

Как видите, obj1 и obj2 выглядят одинаково. Они оба имеют свойство name со значением «Диллион». Но сравнение их --obj1 === obj2--возвращает false. 🤔

То же самое относится и к массивам:

let arr1 = [1, 2, 3]
let arr2 = [1, 2, 3]

console.log(arr1 === arr2)
// false

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

Примитивные и справочные значения

Думайте о примитивном значении как о одном значении (статическом, фиксированном), а о ссылочном значении — как о группе нескольких значений или (динамическом) значении.

Примитивные значения имеют типы string, number, boolean, null, undefined, symbol и BigInt. Эти значения фиксированы и хранятся в стеке, например:

let name = "Dillion"
let age = 60
let isRaining = true

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

let array = [1, 2, 3]
let obj = { name: "Dillion" }
function print() {
  console.log('hello')
}

Эталонное значение — это адрес, указывающий на расположение данных в памяти.

Вот статья, где я объяснил разницу более подробно: Упрощенные примитивные и эталонные значения

Сравнение примитивных и эталонных значений

Когда вы сравниваете примитивные значения, вы сравниваете статические значения, которые имеют фиксированный размер в стеке:

let name = "Dillion"
let name2 = "Dillion"

console.log(name === name2)
// true

Здесь мы сравниваем name и name2, если они равны. Здесь происходит то, что JavaScript проверяет переменные name и name2 в стеке, а затем видит, что они имеют одинаковые значения, так что это правда — они равны.

В случае объектов вы сравниваете ссылки (адреса), а не точные значения. Вот что я имею в виду.

Если у вас есть два таких массива:

let array = [1, 2, 3]
let array2 = [1, 2, 3]

Вот как это будет выглядеть в стеке и куче:

Как вы можете видеть здесь, для array [1, 2, 3] не хранится в стеке. Он хранится в куче, а расположение этих данных в памяти хранится в стеке в качестве ссылки.

То же самое для array2; [1, 2, 3] не хранится в стеке. Он хранится в куче, в другом месте памяти, а ссылка хранится в стеке.

Когда вы сравниваете оба массива, такие как array === array2, вы не совсем сравниваете [1, 2, 3] === [1, 2, 3], а на самом деле сравниваете refForArray === refForArray2 (ref — сокращение для справки).

Как мы видели на иллюстрации кучи, array и array2 имеют разные адреса памяти, что означает, что они имеют разные ссылки, что означает, что переменная array не равна переменной array2.

Единственный способ, которым array и array2 могут быть равны, это если у вас есть что-то вроде:

let array = [1, 2, 3]
let array2 = array

console.log(array === array2)
// true

Назначая array array2, вы назначаете ссылку, которую array содержит в стеке, array2:

Таким образом, array и array2 теперь имеют одинаковое значение — одну и ту же ссылку.

Как сравнивать объекты

Мы видели, что в нашей попытке сравнить два значения объекта мы на самом деле сравниваем ссылку, а не данные объекта. Так как же правильно сравнивать данные объекта?

Есть несколько способов сделать это, но я поделюсь двумя из них.

Сравните объекты, используя _.isEqual из Lodash

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

Более быстрый подход — использовать метод isEqual от Lodash, который является эффективным решением, которое обрабатывает глубокое сравнение между двумя значениями:

const _ = require('lodash'); 

const array = [1, 2, 3]
const object = { name: "Dillion" }

const array2 = [1, 2, 3]
const object2 = { name: "Dillion" }

console.log(_.isEqual(array, array2))
// true

console.log(_.isEqual(object, object2))
/ true

Сравните объекты, используя JSON.stringify()

Допустим, вы не хотите использовать Lodash, вы можете использовать JSON.stringify, который рекурсивно преобразует объект или массив в строку:

const array = [1, 2, 3]
const object = { name: "Dillion" }

const array2 = [1, 2, 3]
const object2 = { name: "Dillion" }

console.log(
  JSON.stringify(array)
  ===
  JSON.stringify(array2)
)
// true

console.log(
  JSON.stringify(object)
  ===
  JSON.stringify(object2)
)
// true

Поскольку строковая версия является примитивным типом (статическим), данные обоих значений можно сравнивать. Но вот ограничение с JSON.stringify().

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

const object = {
  name: "Dillion",
  age: 50
}

const object2 = {
  age: 50,
  name: "Dillion"
}

console.log(
  JSON.stringify(object)
  ===
  JSON.stringify(object2)
)
// false

В object у нас есть name перед age, но в object2 у нас есть age перед name. Это означает, что их строковые представления будут разными, и в результате их данные не равны.

Заворачивать

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

Я надеюсь, что эта статья ответит на вопрос, почему два похожих объекта не равны в JavaScript?. Если это так, пожалуйста, поделитесь этой статьей 🙏🏾