Разработчик 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 ). Если типы операндов совпадают, нет причин не использовать == и != . Возможно, пришло время перестать бояться злых близнецов, которые не так уж злы после того, как вы узнаете их.