Два способа спросить «А эти значения равны?»
В 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.