Два способа спросить «А эти значения равны?»
В JavaScript есть два оператора сравнения на равенство: === (строгое равенство) и == (нестрогое равенство). Выглядят они почти одинаково. А ведут себя - совершенно по-разному.
=== проверяет, что оба операнда имеют одинаковый тип и одинаковое значение. == сначала приводит операнды к общему типу, а потом уже сравнивает. Именно это приведение типов - то самое злополучное coercion - и породило большую часть мемов про равенство в JavaScript.
Если коротко: по умолчанию используйте ===. Но длинную версию стоит прочитать хотя бы раз - чтобы понимать, от чего именно вы отказываетесь.
Строгое равенство: то, что вам почти всегда нужно
Строгое равенство === возвращает true только если совпадают и тип, и значение. Никаких неявных преобразований, никаких сюрпризов:
Если типы не совпадают, ответ сразу false. Если совпадают - JavaScript сравнивает значения. Для примитивов это сравнение по значению, а для объектов - по ссылке (об этом чуть ниже).
!== - это оператор строгого неравенства, он работает по тем же правилам, только наоборот.
Нестрогое равенство: приведение типов под капотом
== допускает, что с обеих сторон стоят значения разных типов. Перед сравнением он запускает целый танец с приведением типов, чтобы привести их к общему знаменателю:
Точные правила расписаны в спецификации, и сами по себе они не то чтобы жуткие - проблема в том, чтобы удержать их в голове посреди отладки. На том, что "0" == false даёт true, спотыкаются даже матёрые разработчики. То же самое с [] == false (тоже true: массив приводится к "", а тот - к 0).
Именно поэтому большинство стайл-гайдов - и правило eqeqeq в ESLint - советуют по умолчанию писать ===. Лишний символ в обмен на правила, которые реально запомнить.
Единственный полезный паттерн с ==
Есть ровно одна идиома с нестрогим равенством, которую стоит держать в арсенале: x == null вернёт true, если x - это null или undefined, и false во всех остальных случаях.
Строгий эквивалент - это x === null || x === undefined. Работает, но выглядит громоздко. Поэтому во многих кодовых базах == null оставляют как единственное узаконенное исключение. Главное - выбрать правило и придерживаться его.
Сравнение объектов в JavaScript идёт по ссылке
Когда речь о объектах, массивах и функциях, и ===, и == задают один и тот же вопрос: «Указывают ли обе стороны на один и тот же объект в памяти?» А вовсе не «одинаковое ли у них содержимое?»
Два объектных литерала с одинаковым содержимым - это всё равно два разных объекта. На этом хоть раз да спотыкается каждый.
Если нужно сравнивать объекты по значению, пишите свой хелпер, подключайте библиотеку (lodash.isequal) или, для простых plain-объектов, сериализуйте через JSON.stringify:
JSON.stringify годится только для простых данных - он игнорирует функции, undefined и символы, а порядок ключей в разных движках для некоторых структур не гарантируется. Это скорее быстрая проверка, чем универсальное решение.
Сравнение NaN в JavaScript
NaN (от «not a number») - это значение, которое JavaScript возвращает, когда у числовой операции нет осмысленного результата: 0/0, Number("abc"), Math.sqrt(-1). Оба оператора равенства возвращают false, если хотя бы с одной стороны стоит NaN - даже когда NaN с обеих сторон:
Чтобы понять, что перед нами NaN, используйте Number.isNaN(value). Старую глобальную функцию isNaN лучше не трогать - она сначала приводит аргумент к числу, поэтому isNaN("hello") вернёт true, а это почти наверняка не то, что вам нужно.
Object.is: почти ===, но с двумя поправками
Object.is(a, b) ведёт себя так же, как ===, за исключением двух пограничных случаев:
В большинстве случаев вам нужен именно ===. К Object.is стоит обращаться, когда нужно, чтобы NaN считался равным самому себе, либо чтобы отличить +0 от -0 - случаи редкие, но иногда критичные: в численных расчётах и во внутренностях фреймворков (React, например, использует Object.is для сравнения состояния).
Знак «не равно» в JavaScript работает по той же логике
!== - строгий, != - нестрогий, и рекомендация та же:
По умолчанию используйте !==. Если в вашем стайл-гайде разрешён == null, то и его «близнец» != null для проверки «не null и не undefined» тоже допустим.
Чек-лист для сравнения значений
Когда нужно сравнить два значения, пройдитесь по этому списку:
- Примитивы одного типа? Берите
===. - Проверка на null или undefined?
x == nullвполне подойдёт, если стайл-гайд не против; иначе пишитеx === null || x === undefined. - Проверка на
NaN?Number.isNaN(x). - Сравниваете объекты по ссылке?
===делает ровно то, что нужно. - Сравниваете объекты по содержимому? Напишите свою функцию, подключите библиотеку или сериализуйте значения. Встроенные операторы тут бессильны.
Держитесь ===, оставьте == как точечный инструмент для случая == null - и вы обойдёте стороной все те подводные камни равенства, которыми забиты FAQ по JavaScript.
Дальше: операторы
Операторы сравнения - это лишь одна грань операторов в JavaScript. В следующем материале разберём всё остальное: арифметические, логические, присваивания и сокращённые формы, которые пригодятся в повседневном коде.
Часто задаваемые вопросы
В чём разница между == и === в JavaScript?
=== - это строгое равенство: возвращает true, только если у операндов совпадает и тип, и значение. == - нестрогое равенство: перед сравнением оно приводит операнды к одному типу. Поэтому 1 === '1' даст false, а 1 == '1' - true, потому что строка сначала преобразуется в число.
Стоит ли всегда использовать === в JavaScript?
По умолчанию - да. === предсказуем, и его правила легко держать в голове. Единственное распространённое исключение - проверка x == null, которая одним махом ловит и null, и undefined. Большинство линтеров (правило eqeqeq в ESLint) требует именно === и разрешает этот паттерн как исключение.
Почему NaN === NaN возвращает false?
По стандарту IEEE 754 NaN не равен ничему, включая самого себя. Поэтому любое сравнение с NaN через операторы равенства вернёт false. Чтобы проверить, является ли значение NaN, используйте Number.isNaN(x) или Object.is(NaN, NaN) - последний, кстати, возвращает true.
Как сравнить два объекта на равенство в JavaScript?
И ==, и === сравнивают объекты по ссылке, а не по содержимому. {a: 1} === {a: 1} даёт false, потому что это два разных объекта в памяти. Для сравнения по значению придётся написать свою функцию, взять что-то вроде isEqual из Lodash или, для простых plain-объектов, сравнить результат JSON.stringify.