Разработчик JavaScript Дуглас Крокфорд назвал операторы JavaScript ==
и !=
Злыми близнецами, которых следует избегать . Однако, как только вы их поймете, эти операторы не так уж плохи и могут быть полезны. В этой статье рассматриваются ==
и !=
, Объясняется, как они работают, и вы узнаете их лучше.
Проблемные операторы ==
и !=
Язык JavaScript включает в себя два набора операторов равенства: ===
и !==
, и
==
и !=
. Понимание того, почему существуют два набора операторов равенства, и выяснение, какие из них использовать, в каких ситуациях вызывало много путаницы.
Операторы ===
и !==
не сложны для понимания. Если оба операнда имеют одинаковый тип и имеют одинаковое значение, ===
возвращает значение true
, тогда как !==
возвращает значение false
. Однако когда значения или типы различаются, ===
возвращает false
а !==
возвращает true
.
Операторы ==
и !=
Ведут себя одинаково, когда оба операнда имеют одинаковый тип. Однако, когда типы различаются, JavaScript приводит операнд к другому типу, чтобы сделать операнды совместимыми перед сравнением. Результаты часто сбивают с толку, как показано ниже:
"this_is_true" == false // false "this_is_true" == true // false
Поскольку существует только два возможных логических значения, вы можете подумать, что одно из выражений должно иметь значение true
. Тем не менее, они оба оценивают как false
. Дополнительная путаница возникает, когда вы предполагаете, что переходные отношения (если a равно b, а b равно c, тогда a равно c):
'' == 0 // true 0 == '0' // true '' == '0' // false
Этот пример показывает, что ==
не хватает транзитивности. Если пустая строка равна числу 0, и если число 0 равно строке, состоящей из символа 0, тогда пустая строка должна соответствовать строке, состоящей из 0. Но это не так.
Столкнувшись с несовместимыми типами при сравнении операндов с помощью ==
или !=
, JavaScript приводит один тип к другому, чтобы сделать их сопоставимыми. Напротив, он никогда не выполняет приведение типов (что приводит к несколько лучшей производительности) при использовании ===
и !==
. Из-за разных типов ===
всегда возвращает false
во втором примере.
Понимание правил, управляющих тем, как JavaScript приводит операнд к другому типу, так что оба операнда совместимы по типу до применения ==
и !=
, Может помочь вам определить, когда более уместно использовать ==
и !=
, И чувствовать себя уверенно используя эти операторы. В следующем разделе мы рассмотрим правила принуждения, которые используются с операторами ==
и !=
.
Как работают ==
и !=
Работают?
Лучший способ узнать, как работают ==
и !=
— изучить спецификацию языка ECMAScript. Этот раздел посвящен ECMAScript 262 . Раздел 11.9 спецификации посвящен операторам равенства.
Операторы ==
и !=
Появляются в грамматических произведениях EqualityExpression
и EqualityExpressionNoIn
. (В отличие от первого производства, второе производство избегает оператора in
.) Давайте рассмотрим производство EqualityExpression
, показанное ниже.
EqualityExpression : RelationalExpression EqualityExpression == RelationalExpression EqualityExpression != RelationalExpression EqualityExpression === RelationalExpression EqualityExpression !== RelationalExpression
Согласно этой постановке выражение равенства — это либо выражение отношения, выражение равенства, равное выражению отношения через ==
, выражение равенства, не равное выражению отношения через !=
, И так далее. (Я игнорирую ===
и !==
, которые не имеют отношения к этой статье.)
В разделе 11.9.1 представлена следующая информация о том, как работает ==
:
Производство EqualityExpression: EqualityExpression == RelationalExpression оценивается следующим образом:
- Пусть lref будет результатом вычисления EqualityExpression .
- Пусть lval будет GetValue ( lref ).
- Пусть rref будет результатом вычисления выражения RelationalExpression .
- Пусть rval будет GetValue ( rref ).
- Вернуть результат выполнения абстрактного сравнения на равенство rval == lval . (См. 11.9.3.)
В разделе 11.9.2 представлена аналогичная информация о том, как !=
Работает:
Производство EqualityExpression: EqualityExpression ! = RelationalExpression оценивается следующим образом:
- Пусть lref будет результатом вычисления EqualityExpression .
- Пусть lval будет GetValue ( lref ).
- Пусть rref будет результатом вычисления выражения RelationalExpression .
- Пусть rval будет GetValue ( rref ).
- Пусть r будет результатом выполнения абстрактного сравнения на равенство rval ! = Lval . (См. 11.9.3.)
- Если r верно , вернуть false . В противном случае верните true .
lref
и rref
являются ссылками на левую и правую части операторов ==
и !=
. Каждая ссылка передается внутренней функции GetValue()
для возврата соответствующего значения.
Суть работы ==
и !=
Определяется алгоритмом сравнения абстрактного равенства, который представлен в разделе 11.9.3:
Сравнение
x == y
, гдеx
иy
являются значениями, дает
правда или ложь . Такое сравнение выполняется следующим образом:
- Если тип (
x
) совпадает с типом (y
), то
- Если Тип (
x
) не определен, вернуть true .- Если Type (
x
) равен Null, вернуть true .- Если тип (
x
) является число, то
- Если
x
равен NaN , вернуть false .- Если
y
равен NaN , вернуть false .- Если x — это то же числовое значение, что и y , вернуть true .
- Если x равен +0, а y равен -0 , вернуть true .
- Если x равен -0, а y равен +0 , вернуть true .
- Вернуть ложь .
- Если Type (
x
) — String, тогда вернуть true, еслиx
иy
— это абсолютно одинаковая последовательность символов (одинаковая длина и одинаковые символы в соответствующих позициях). В противном случае верните false .- Если Type (
x
) — Boolean, вернуть true, еслиx
иy
оба — true или оба false . В противном случае верните false .- Верните true, если
x
иy
относятся к одному и тому же объекту. В противном случае верните false .- Если
x
равно нулю, аy
не определено , верните true .- Если
x
не определено, аy
равно нулю , верните true.- Если Type (
x
) равен Number, а Type (y
) равен String, вернуть результат сравненияx
== ToNumber (y
).- Если Type (
x
) равен String, а Type (y
) равен Number, вернуть результат сравнения ToNumber (x
) ==y
.- Если Type (
x
) — Boolean, вернуть результат сравнения ToNumber (x
) ==y
.- Если Type (
y
) — Boolean, вернуть результат сравненияx
== ToNumber (y
).- Если Type (
x
) равен либо String, либо Number, а Type (y
) равен Object, возвращает результат сравненияx
== ToPrimitive (y
).- Если Type (
x
) равен Object, а Type (y
) является либо String, либо Number, вернуть результат сравнения ToPrimitive (x
) ==y
.- Вернуть ложь .
Шаг 1 в этом алгоритме выполняется, когда типы операндов совпадают. Это показывает, что undefined
равно undefined
а значение равно null
. Он также показывает, что ничто не равно NaN
(не число), два одинаковых числовых значения равны, +0 равно -0, две строки одинаковой длины и последовательности символов равны, true
равна true
и false
равна false
, и две ссылки на один и тот же объект равен.
Шаги 2 и 3 показывают, почему null != undefined
возвращает false
. JavaScript считает эти значения одинаковыми.
Начиная с шага 4, алгоритм становится интересным. Этот шаг фокусируется на равенстве между значениями Number и String. Когда первый операнд представляет собой число, а второй операнд представляет собой строку, второй операнд преобразуется в число с помощью внутренней функции ToNumber()
. Выражение x
== ToNumber ( y
) указывает на рекурсию; алгоритм, начинающийся в разделе 11.9.1, применяется повторно.
Шаг 5 эквивалентен шагу 4, но первый операнд имеет тип String и должен быть преобразован в тип Number.
Шаги 6 и 7 преобразуют логический операнд в числовой тип и рекурсивно. Если другой операнд является логическим, он будет преобразован в число при следующем выполнении этого алгоритма, который будет повторяться еще раз. С точки зрения производительности, вы можете убедиться, что оба операнда имеют логический тип, чтобы избежать обоих шагов рекурсии.
Шаг 9 показывает, что если любой из операндов имеет тип объекта, этот операнд преобразуется в примитивное значение через
Внутренняя функция ToPrimitive()
и алгоритм рекурсивно.
Наконец, алгоритм считает оба операнда неравными и возвращает false
на шаге 10.
Несмотря на подробности, алгоритм сравнения абстрактного равенства довольно прост в использовании. Однако это относится к паре внутренних функций, ToNumber()
и ToPrimitive()
, чьи внутренние работы должны быть раскрыты для полного понимания алгоритма.
Функция ToNumber()
преобразует свой аргумент в число и описана в разделе 9.3. В следующем списке приведены возможные нечисловые аргументы и эквивалентные возвращаемые значения:
- Если аргумент не определен, вернуть NaN .
- Если аргумент равен Null, вернуть +0 .
- Если аргумент имеет логическое значение true, вернуть 1 . Если аргумент имеет логическое значение false, вернуть +0 .
- Если аргумент имеет тип Number, то входной аргумент возвращается — преобразование не выполняется.
- Если аргумент имеет строковый тип, то применяется раздел 9.3.1 «ToNumber, примененный к строковому типу». Возвращается числовое значение, соответствующее строковому аргументу, как указано в грамматике. Если аргумент не соответствует указанной грамматике, возвращается NaN. Например, аргумент
"xyz"
приводит к возвращению NaN. Кроме того, аргумент"29"
приводит к возвращению 29. - Если аргумент имеет тип объекта, примените следующие шаги:
- Пусть primValue будет ToPrimitive ( входной аргумент , номер подсказки).
- Вернуть ToNumber ( primValue ).
Функция ToPrimitive()
принимает входной аргумент и необязательный аргумент PreferredType. Входной аргумент преобразуется в необъектный тип. Если объект способен конвертировать в более чем один тип примитива, ToPrimitive()
использует необязательную подсказку PreferredType для предпочтения предпочтительного типа. Преобразование происходит следующим образом:
- Если входной аргумент не определен, то возвращается входной аргумент (не определен) — преобразование не выполняется.
- Если входным аргументом является NULL, то входной аргумент (NULL) возвращается — преобразование не выполняется.
- Если входной аргумент имеет логический тип, то входной аргумент возвращается — преобразование не выполняется.
- Если входной аргумент имеет тип Number, то входной аргумент возвращается — преобразование не выполняется.
- Если входной аргумент имеет тип String, то входной аргумент возвращается — преобразование не выполняется.
- Если входной аргумент имеет тип объекта, то возвращается значение по умолчанию, соответствующее входному аргументу. Значение объекта по умолчанию извлекается путем вызова внутреннего метода
[[DefaultValue]]
объекта с передачей необязательной подсказки PreferredType. Поведение[[DefaultValue]]
определено для всех собственных объектов ECMAScript в Разделе 8.12.8.
В этом разделе представлено довольно много теории. В следующем разделе мы перейдем к практическому, представив различные выражения, включающие ==
и !=
И пройдя шаги алгоритма для их оценки.
Знакомство со злыми близнецами
Теперь, когда мы знаем, как ==
и !=
Работают в соответствии со спецификацией ECMAScript, давайте хорошо применим эти знания, исследуя различные выражения с участием этих операторов. Мы рассмотрим, как оцениваются эти выражения, и выясним, почему они являются true
или false
.
Для моего первого примера рассмотрим следующую пару или выражения, которые были представлены в начале статьи:
"this_is_true" == false // false "this_is_true" == true // false
Выполните следующие шаги для оценки этих выражений в соответствии с алгоритмом сравнения абстрактного равенства:
- Пропустите Шаг 1, потому что типы разные:
typeof "this_is_true"
возвращает"string"
аtypeof false
илиtypeof true
возвращает"boolean"
. - Пропустите шаги со 2 по 6, которые не применяются, поскольку они не соответствуют типам операндов. Однако, Шаг 7 применяется, потому что правильный аргумент имеет тип Boolean. Выражения преобразуются в
"this_is_true" == ToNumber(false)
и"this_is_true" == ToNumber(true)
. -
ToNumber(false)
возвращает +0, аToNumber(true)
возвращает 1, что уменьшает выражения до"this_is_true" == +0
и"this_is_true" == 1
соответственно. На этом этапе алгоритм возвращается. - Пропустите шаги с 1 по 4, которые не применяются. Однако шаг 5 применяется, потому что левый операнд имеет тип String, а правый операнд имеет тип Number. Выражения преобразуются в
ToNumber("this_is_true") == +0
иToNumber("this_is_true") == 1
. -
ToNumber("this_is_true")
возвращает NaN, что сокращает выражения доNaN == +0
иNaN == 1
соответственно. На этом этапе алгоритм возвращается. - Шаг 1 введен, потому что каждый из NaN, +0 и 1 имеет тип Number. Шаги 1.a и 1.b пропускаются, потому что они не применяются. Тем не менее, шаг 1.ci применяется, потому что левый операнд NaN. Алгоритм теперь возвращает false (NaN не равен ничему, включая себя) в качестве значения каждого исходного выражения и перематывает стек, чтобы полностью выйти из рекурсии.
Мой второй пример (который основан на значении жизни в соответствии с «Путеводителем автостопом по Галактике» ) сравнивает объект с числом через ==
, возвращая значение true
:
var lifeAnswer = { toString: function() { return "42"; } }; alert(lifeAnswer == 42);
Следующие шаги показывают, как JavaScript использует алгоритм сравнения абстрактного равенства, чтобы получить значение true в качестве значения выражения:
- Пропустите шаги с 1 по 8, которые не применяются, поскольку они не соответствуют типам операндов. Однако шаг 9 применяется, потому что левый операнд имеет тип Object, а правый операнд имеет тип Number. Выражение преобразуется в
ToPrimitive(lifeAnswer) == 42
. -
ToPrimitive()
вызывает внутренний методlifeAnswer
[[DefaultValue]]
без подсказки. В соответствии с разделом 8.12.8 в спецификации ECMAScript 262[[DefaultValue]]
вызывает методtoString()
, который возвращает"42"
. Выражение преобразуется в"42" == 42
и алгоритм возвращается. - Пропустите шаги с 1 по 4, которые не применяются, поскольку они не соответствуют типам операндов. Однако шаг 5 применяется, потому что левый операнд имеет тип String, а правый операнд имеет тип Number. Выражение преобразуется в
ToNumber("42") == 42
. -
ToNumber("42")
возвращает 42, и выражение преобразуется в 42 == 42. Алгоритм повторяется и выполняется шаг 1.c.iii. Поскольку числа одинаковы, возвращаетсяtrue
и рекурсия раскручивается.
В моем последнем примере давайте выясним, почему следующая последовательность не демонстрирует транзитивность, в которой третье сравнение будет возвращать true
вместо false
:
'' == 0 // true 0 == '0' // true '' == '0' // false
Следующие шаги показывают, как JavaScript использует алгоритм сравнения абстрактного равенства, чтобы получить значение true
как значение '' == 0
.
- Шаг 5 выполняется, в результате чего
ToNumber('') == 0
, который преобразуется в0 == 0
и алгоритм возвращается. (Раздел 9.3.1 в спецификации гласит, что MV [математическое значение] StringNumericLiteral ::: [empty] равно 0. Другими словами, числовое значение пустой строки равно 0.) - Выполняется шаг 1.c.iii, который сравнивает 0 с 0 и возвращает
true
(и раскручивает рекурсию).
Следующие шаги показывают, как JavaScript использует алгоритм сравнения абстрактного равенства, чтобы получить значение true
0 == '0'
:
- Шаг 4 выполняется, в результате чего
0 == ToNumber('0')
, который преобразуется в0 == 0
и алгоритм повторяется. - Выполняется шаг 1.c.iii, который сравнивает 0 с 0 и возвращает
true
(и раскручивает рекурсию).
Наконец, JavaScript выполняет Шаг 1.d в алгоритме сравнения абстрактного равенства, чтобы получить значение true
как значение '' == '0'
. Поскольку две строки имеют разную длину (0 и 1), возвращается false
.
Вывод
Возможно, вам интересно, почему вы должны беспокоиться о ==
и !=
. В конце концов, предыдущие примеры показали, что эти операторы могут быть медленнее, чем их ===
и !==
аналоги из-за приведения типов и рекурсии. Возможно, вы захотите использовать ==
и !=
Потому что существуют контексты, где ===
и !==
дают никаких преимуществ. Рассмотрим следующий пример:
typeof lifeAnswer === "object" typeof lifeAnswer == "object"
Оператор typeof
возвращает строковое значение. Поскольку строковое значение сравнивается с другим строковым значением ( "object"
), никакого приведения типов не происходит, и ==
столь же эффективен, как и ===
. Возможно, новички в JavaScript, которые никогда не сталкивались с ===
, найдут такой код более понятным. Аналогично, следующий фрагмент кода не требует приведения типов (оба операнда имеют тип Number), и поэтому !=
Не менее эффективен, чем !==
:
array.length !== 3 array.length != 3
Эти примеры показывают, что ==
и !=
Подходят для сравнений, которые не требуют принуждения. Когда типы операндов различны, ===
и !==
— путь, потому что они возвращают false
а не неожиданные значения (например, false == ""
возвращает true
). Если типы операндов совпадают, нет причин не использовать ==
и !=
. Возможно, пришло время перестать бояться злых близнецов, которые не так уж злы после того, как вы узнаете их.