Главная / Статьи

Объяснение операций сравнения в JavaScript: ===, ==, операторы отношений и Object.is

Узнайте, как строгое и нестрогое равенство, относительные сравнения и Object.is на самом деле определяют идентичность в JavaScript, а также какие ловушки принудительной трансформации скрываются за каждым из них.

Издатель: REACTAPP.TOP10 мин чтения2405 слов

Источники: Day 4 of Learning JavaScript: The Three Ways JavaScript Asks “Are These the Same Thing?”

Представьте форму, в которой пользователь вводит свой возраст в виде текста "18", и проверка на соответствие числу 18 проходит без проблем, но позже арифметические операции с этим значением дают неверный результат. Или проверка доступа проходит успешно там, где это не должно быть так, потому что два разных значения случайно преобразовались в одинаковое. Подобные ошибки возникают из-за того, что у JavaScript есть несколько способов определения того, являются ли два значения одинаковыми, причем каждый из них понимает под «одинаковыми» что-то свое. Прочитав это, вы будете знать, что на самом деле делают ===, ==, операторы отношений и Object.is(), а также какой из них следует использовать в каждой ситуации.

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

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

Строгое равенство (===) в качестве стандарта

Сначала проверка типа, затем проверка значения

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

console.log(1 === 1);
// true (both are numbers, both are 1)

console.log(1 === "1");
// false (the types fail immediately: number versus string)

console.log(0 === false);
// false (number versus boolean)

console.log("" === false);
// false (string versus boolean)

console.log(null === null);
// true

console.log(null === undefined);
// false (different types)

Каждое значение false в этом фрагменте кода возникает в результате сравнения разных типов: число с строкой, число с логическим значением, строка с логическим значением, null с undefined.

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

console.log("hello" === "Hello");
// false (different capitalization)

console.log("john@email.com" === "john@email.com ");
// false (the second string has a hidden space at the end)

На практике это означает нормализацию входных данных пользователя, обычно с использованием метода trim(), а также часто toLowerCase() для полей, нечувствительных к регистру, прежде чем проводить сравнение.

Два преднамеренных исключения из стандарта IEEE 754

Строгое сравнение равенства является надежным практически везде, за исключением двух случаев с числами. Ни один из этих случаев не является ошибкой; оба соответствуют стандарту IEEE 754 для чисел с плавающей точкой, на котором основаны числа в JavaScript. Первый случай — это значение NaN, означающее «не число», которое не равно ничему, даже самому себе.

const result = 0 / 0;
// result is NaN

console.log(result === NaN);
// false

console.log(NaN === NaN);
// false

Поскольку проверка равенства никогда не сможет обнаружить это, вместо нее следует использовать специальную функцию Number.isNaN().

console.log(Number.isNaN(NaN));
// true

Преферируйте ее старой глобальной функции isNaN(), которая сначала преобразует аргумент в число, поэтому для строк вроде "abc" возвращает true. Второй исключительный случай связан с нулем: JavaScript отдельно представляет положительный и отрицательный нули, что имеет значение в некоторых числовых алгоритмах, однако строгое сравнение равенства считает их равными.

console.log(+0 === -0);
// true

Объекты сравниваются по идентичности

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

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

const userA = { name: "Alex" };
const userB = { name: "Alex" };

console.log(userA === userB);
// false (two different objects sitting in different memory slots)

console.log([] === []);
// false (two different arrays sitting in different memory slots)

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

const original = { name: "Alex" };
const pointer = original;

console.log(original === pointer);
// true (both point to the exact same memory location)

Чтобы проверить, содержат ли два разных объекта одинаковые данные, сравните сами их примитивные свойства.

console.log(userA.name === userB.name);
// true (now we are comparing two strings, not two object addresses)

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

Строгое неравенство (!==)

Проверка на различие значений так же распространена, как и проверка на их совпадение. Строгое неравенство — это прямое отрицание оператора ===: оно возвращает true, когда отличаются либо типы, либо значения.

console.log(1 !== 2);
// true (they are different numbers)

console.log(1 !== "1");
// true (one is a number, one is a string)

console.log("apple" !== "orange");
// true

Этот оператор идеально подходит там, где код должен выполняться только в случае, если условие не выполнено, например при проверке прав или фильтрации нежелательных элементов.

const currentRole = "guest";

if (currentRole !== "admin") {
  console.log("Access denied. Admins only.");
}
// "Access denied. Admins only."

Расслабленное сравнение (==) и его преобразования

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

До того, как строгое сравнение стало нормой, в большинстве кодов использовалось ==. Когда типы операндов различаются, слабое сравнение преобразует одно или оба значения в общий тип, а затем сравнивает их. Результаты такого сравнения быстро противоречат интуиции.

console.log(1 == "1");
// true (the string "1" becomes the number 1)

console.log(0 == false);
// true (false becomes 0)

console.log("" == false);
// true (empty string and false both become 0)

console.log(0 == "");
// true (empty string becomes 0)

console.log([] == "");
// true (empty array becomes empty string)

console.log([1] == 1);
// true (array becomes "1", which becomes 1)

Известный пример показывает, сколько скрытых шагов может быть в процессе сравнения.

console.log([] == 0);
// true

Чтобы получить значение true, движок сначала преобразует пустой массив в примитивный тип, в результате чего получается пустая строка "". Типы всё ещё различаются, поскольку сравнивается строка и число, поэтому строка преобразуется в число, что даёт значение 0. Итоговое сравнение — 0 == 0, что равносильно true.

Логические значения также представляют сложности, поскольку при сравнении с не-логическими значениями они сначала преобразуются в числа. true превращается в 1.

console.log(true == "true");
// false

Строка "true" не превращается в 1; при конвертации в число она становится NaN, и 1 не равно NaN. Именно поэтому выражение value == true почти никогда не является тем, что вам нужно.

null и undefined подчиняются особому правилу: при использовании слабого сравнения они равны друг другу и ничему больше, даже не 0, false или пустой строке.

console.log(null == undefined);
// true

console.log(null == 0);
// false

console.log(null == false);
// false

console.log(null == "");
// false

console.log(undefined == 0);
// false

Проверка на null, когда == допустима

В современном коде лучше избегать слабого сравнения, за одним распространённым исключением. При обработке данных из API часто необходимо определить, является ли значение null или undefined, и выражение == null позволяет ответить на этот вопрос за одно сравнение.

function checkStatus(status) {
  if (status == null) {
    console.log("Status is missing entirely");
  } else {
    console.log("Status is:", status);
  }
}

checkStatus(null);
// "Status is missing entirely"

checkStatus(undefined);
// "Status is missing entirely"

checkStatus(0);
// "Status is: 0"

Это работает именно благодаря вышеуказанному правилу: null считается эквивалентным только null и undefined, поэтому ложные, но значимые значения, такие как 0 или "", проходят незатронутыми. Этот синтаксис заменяет более длинную конструкцию status === null || status === undefined. Имейте в виду, что многие команды настраивают инструменты проверки кода так, чтобы полностью запретить использование ==, в таком случае необходимо писать явную строгую версию оператора. Например, правило eqeqeq в ESLint имеет опцию, которая разрешает именно такое сравнение null, запрещая при этом другие его использования. Во многих случаях оператор объединения значений при наличии null ?? или оператор цепочечного доступа с проверкой наличия ?. выражают задуманное ещё более прямо.

Расслабленное неравенство (!=)

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

console.log(1 != "1");
// false (it converts "1" to a number, decides they are the same, so they are not different)

Здесь "1" преобразуется в число 1, значения считаются равными, и проверка «неравенства» возвращает false. Относитесь к != с той же осторожностью, что и к ==, и в качестве альтернативы используйте !==.

Сравнения отношений: <, >, <= и >=

Числа и смешанные типы

При работе с двумя числами операторы отношений ведут себя точно так же, как и в арифметике.

console.log(5 > 3);
// true

console.log(10 <= 10);
// true

Когда строка встречается с числом, сначала строка преобразуется в число. Строка, не представляющая собой число, становится NaN, и любое сравнение отношений, в котором участвует NaN, возвращает false.

console.log("10" > 5);
// true

console.log("abc" > 3);
// false ("abc" becomes NaN, and NaN comparisons always return false)

Где различаются null и undefined

Операторы сравнения показывают несоответствие при использовании слабого равенства. == отказывается преобразовывать null в число, тогда как <, >, <= и >= преобразуют его в 0.

console.log(null == 0);
// false

console.log(null > 0);
// false (0 is not greater than 0)

console.log(null >= 0);
// true (0 is greater than or equal to 0)

Следовательно, null не равен ни 0, ни больше 0, но в то же время он больше или равен 0. Последний результат становится понятным, если знать, что выражение a >= b оценивается как «a < b не является истинным», а 0 < 0 — ложным.

undefined ведет себя иначе: преобразуясь в число, он становится NaN, поэтому любое сравнение, включающее undefined, даёт результат false.

console.log(undefined > 0);
// false

console.log(undefined < 0);
// false

Сравнение строк происходит по кодовым единицам, а не в порядке словаря

Когда оба операнда являются строками, не происходит их числового преобразования. Строки сравниваются символ за символом слева направо, используя числовые кодовые единицы UTF-16 каждого символа.

console.log("apple" < "banana");
// true (a comes before b)

console.log("b" > "a");
// true (b comes after a)

Буквы заглавного размера имеют меньшие кодовые единицы, чем буквы строчного размера, что объясняет, почему простой алгоритм сортировки может поместить «Zebra» перед «apple».

console.log("Z" < "a");
// true (Z=90, a=97 in Unicode)

Для сортировки, ориентированной на человека, используйте localeCompare() или Intl.Collator, которые следуют языковым правилам вместо простых кодовых единиц.

Ещё одна опасная ситуация возникает при работе со строками, содержащими цифры. При сравнении как строк «10» считается меньше, чем «9».

console.log("10" < "9");
// true (the character "1" is smaller than "9")

У JavaScript нет ни малейшего представления о том, что эти строки обозначают числовые значения. Он смотрит на первый символ каждой из них — "1" против "9", видит, что "1" идет раньше, и на этом останавливается, возвращая значение true. То же самое происходит при сортировке массива чисел с помощью метода sort() без указания функции сравнения, поскольку стандартная сортировка преобразует элементы в строки перед их сравнением; для числовой сортировки необходимо передать функцию (a, b) => a - b.

Решение заключается в явном преобразовании числовых строк в числа перед сравнением их значений.

console.log(Number("10") < Number("9"));
// false (correct: 10 is not less than 9)

Object.is() для точного совпадения

Устранение недостатков оператора ===

Object.is() — это наиболее точная проверка равенства, предоставляемая JavaScript. Для обычных значений она даёт те же результаты, что и оператор строгого сравнения.

console.log(Object.is(1, 1));
// true

console.log(Object.is(1, "1"));
// false

Различия заключаются именно в двух случаях, предусмотренных стандартом IEEE 754. Функция Object.is() считает NaN равным самому себе:

console.log(Object.is(NaN, NaN));
// true

Кроме того, она различает положительное ноль и отрицательный ноль:

console.log(Object.is(0, -0));
// false

Три проверки одновременно

  • 1 и "1": оператор == возвращает true; операторы === и Object.is() возвращают false.
  • 0 и false: оператор == возвращает true; операторы === и Object.is() возвращают false.
  • null и undefined: оператор == возвращает true; операторы === и Object.is() возвращают false.
  • NaN и NaN: операторы == и === возвращают false; функция Object.is() возвращает true.
  • +0 и -0: операторы == и === возвращают true; функция Object.is() возвращает false.
  • [] и [], или {} и {}: все три оператора возвращают false, поскольку это разные объекты.
  • 5 и 5: все три оператора возвращают true.
  • Вы редко будете вызывать Object.is() напрямую, но он важен для инструментов, которыми вы пользуетесь ежедневно. React использует Object.is(), чтобы определить, изменилось ли состояние или какая-либо зависимость и требуется ли перерисовка, что также объясняет, почему изменение объекта на месте не вызывает обновления: ссылка остается идентичной. Библиотеки для управления состоянием применяют ту же идею; для более подробного изучения см. как React-Redux использует проверки равенства для принятия решений о перерисовке.

    Основные выводы

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

    1. По умолчанию везде используйте === и !==.
    2. Избегайте использования == и !=, за исключением специального сокращения value == null, если это разрешено правилами проверки кода.
    3. Для обнаружения значений NaN используйте функцию Number.isNaN(), а не операторы равенства.
    4. Помните, что при сравнении объектов проверяется их идентичность, а не содержимое.
    5. Перед использованием операторов < или > преобразуйте строковые значения в числа с помощью функции Number(), а также укажите функцию-сравнитель для метода sort().
    6. Используйте функцию Object.is(), когда необходимо точное обработание значений NaN или числа нуля с знаком.

    Функции гибкого преобразования в JavaScript не исчезнут, но использование строгих операторов сравнения поможет избежать их непреднамеренного применения в рабочем коде.