Статьи

Не бойтесь злых близнецов (операторы == и! =)

Разработчик 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 оценивается следующим образом:

  1. Пусть lref будет результатом вычисления EqualityExpression .
  2. Пусть lval будет GetValue ( lref ).
  3. Пусть rref будет результатом вычисления выражения RelationalExpression .
  4. Пусть rval будет GetValue ( rref ).
  5. Вернуть результат выполнения абстрактного сравнения на равенство rval == lval . (См. 11.9.3.)

В разделе 11.9.2 представлена ​​аналогичная информация о том, как != Работает:

Производство EqualityExpression: EqualityExpression ! = RelationalExpression оценивается следующим образом:

  1. Пусть lref будет результатом вычисления EqualityExpression .
  2. Пусть lval будет GetValue ( lref ).
  3. Пусть rref будет результатом вычисления выражения RelationalExpression .
  4. Пусть rval будет GetValue ( rref ).
  5. Пусть r будет результатом выполнения абстрактного сравнения на равенство rval ! = Lval . (См. 11.9.3.)
  6. Если r верно , вернуть false . В противном случае верните true .

lref и rref являются ссылками на левую и правую части операторов == и != . Каждая ссылка передается внутренней функции GetValue() для возврата соответствующего значения.

Суть работы == и != Определяется алгоритмом сравнения абстрактного равенства, который представлен в разделе 11.9.3:

Сравнение x == y , где x и y являются значениями, дает
правда или ложь . Такое сравнение выполняется следующим образом:

  1. Если тип ( x ) совпадает с типом ( y ), то
    1. Если Тип ( x ) не определен, вернуть true .
    2. Если Type ( x ) равен Null, вернуть true .
    3. Если тип ( x ) является число, то
      1. Если x равен NaN , вернуть false .
      2. Если y равен NaN , вернуть false .
      3. Если x — это то же числовое значение, что и y , вернуть true .
      4. Если x равен +0, а y равен -0 , вернуть true .
      5. Если x равен -0, а y равен +0 , вернуть true .
      6. Вернуть ложь .
    4. Если Type ( x ) — String, тогда вернуть true, если x и y — это абсолютно одинаковая последовательность символов (одинаковая длина и одинаковые символы в соответствующих позициях). В противном случае верните false .
    5. Если Type ( x ) — Boolean, вернуть true, если x и y оба — true или оба false . В противном случае верните false .
    6. Верните true, если x и y относятся к одному и тому же объекту. В противном случае верните false .
  2. Если x равно нулю, а y не определено , верните true .
  3. Если x не определено, а y равно нулю , верните true.
  4. Если Type ( x ) равен Number, а Type ( y ) равен String, вернуть результат сравнения x == ToNumber ( y ).
  5. Если Type ( x ) равен String, а Type ( y ) равен Number, вернуть результат сравнения ToNumber ( x ) == y .
  6. Если Type ( x ) — Boolean, вернуть результат сравнения ToNumber ( x ) == y .
  7. Если Type ( y ) — Boolean, вернуть результат сравнения x == ToNumber ( y ).
  8. Если Type ( x ) равен либо String, либо Number, а Type ( y ) равен Object, возвращает результат сравнения x == ToPrimitive ( y ).
  9. Если Type ( x ) равен Object, а Type ( y ) является либо String, либо Number, вернуть результат сравнения ToPrimitive ( x ) == y .
  10. Вернуть ложь .

Шаг 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.
  • Если аргумент имеет тип объекта, примените следующие шаги:
    1. Пусть primValue будет ToPrimitive ( входной аргумент , номер подсказки).
    2. Вернуть ToNumber ( primValue ).

Функция ToPrimitive() принимает входной аргумент и необязательный аргумент PreferredType. Входной аргумент преобразуется в необъектный тип. Если объект способен конвертировать в более чем один тип примитива, ToPrimitive() использует необязательную подсказку PreferredType для предпочтения предпочтительного типа. Преобразование происходит следующим образом:

  1. Если входной аргумент не определен, то возвращается входной аргумент (не определен) — преобразование не выполняется.
  2. Если входным аргументом является NULL, то входной аргумент (NULL) возвращается — преобразование не выполняется.
  3. Если входной аргумент имеет логический тип, то входной аргумент возвращается — преобразование не выполняется.
  4. Если входной аргумент имеет тип Number, то входной аргумент возвращается — преобразование не выполняется.
  5. Если входной аргумент имеет тип String, то входной аргумент возвращается — преобразование не выполняется.
  6. Если входной аргумент имеет тип объекта, то возвращается значение по умолчанию, соответствующее входному аргументу. Значение объекта по умолчанию извлекается путем вызова внутреннего метода [[DefaultValue]] объекта с передачей необязательной подсказки PreferredType. Поведение [[DefaultValue]] определено для всех собственных объектов ECMAScript в Разделе 8.12.8.

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

Знакомство со злыми близнецами

Теперь, когда мы знаем, как == и != Работают в соответствии со спецификацией ECMAScript, давайте хорошо применим эти знания, исследуя различные выражения с участием этих операторов. Мы рассмотрим, как оцениваются эти выражения, и выясним, почему они являются true или false .

Для моего первого примера рассмотрим следующую пару или выражения, которые были представлены в начале статьи:

 "this_is_true" == false // false "this_is_true" == true // false 

Выполните следующие шаги для оценки этих выражений в соответствии с алгоритмом сравнения абстрактного равенства:

  1. Пропустите Шаг 1, потому что типы разные: typeof "this_is_true" возвращает "string" а typeof false или typeof true возвращает "boolean" .
  2. Пропустите шаги со 2 по 6, которые не применяются, поскольку они не соответствуют типам операндов. Однако, Шаг 7 применяется, потому что правильный аргумент имеет тип Boolean. Выражения преобразуются в "this_is_true" == ToNumber(false) и "this_is_true" == ToNumber(true) .
  3. ToNumber(false) возвращает +0, а ToNumber(true) возвращает 1, что уменьшает выражения до "this_is_true" == +0 и "this_is_true" == 1 соответственно. На этом этапе алгоритм возвращается.
  4. Пропустите шаги с 1 по 4, которые не применяются. Однако шаг 5 применяется, потому что левый операнд имеет тип String, а правый операнд имеет тип Number. Выражения преобразуются в ToNumber("this_is_true") == +0 и ToNumber("this_is_true") == 1 .
  5. ToNumber("this_is_true") возвращает NaN, что сокращает выражения до NaN == +0 и NaN == 1 соответственно. На этом этапе алгоритм возвращается.
  6. Шаг 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. Пропустите шаги с 1 по 8, которые не применяются, поскольку они не соответствуют типам операндов. Однако шаг 9 применяется, потому что левый операнд имеет тип Object, а правый операнд имеет тип Number. Выражение преобразуется в ToPrimitive(lifeAnswer) == 42 .
  2. ToPrimitive() вызывает внутренний метод lifeAnswer [[DefaultValue]] без подсказки. В соответствии с разделом 8.12.8 в спецификации ECMAScript 262 [[DefaultValue]] вызывает метод toString() , который возвращает "42" . Выражение преобразуется в "42" == 42 и алгоритм возвращается.
  3. Пропустите шаги с 1 по 4, которые не применяются, поскольку они не соответствуют типам операндов. Однако шаг 5 применяется, потому что левый операнд имеет тип String, а правый операнд имеет тип Number. Выражение преобразуется в ToNumber("42") == 42 .
  4. ToNumber("42") возвращает 42, и выражение преобразуется в 42 == 42. Алгоритм повторяется и выполняется шаг 1.c.iii. Поскольку числа одинаковы, возвращается true и рекурсия раскручивается.

В моем последнем примере давайте выясним, почему следующая последовательность не демонстрирует транзитивность, в которой третье сравнение будет возвращать true вместо false :

 '' == 0 // true 0 == '0' // true '' == '0' // false 

Следующие шаги показывают, как JavaScript использует алгоритм сравнения абстрактного равенства, чтобы получить значение true как значение '' == 0 .

  1. Шаг 5 выполняется, в результате чего ToNumber('') == 0 , который преобразуется в 0 == 0 и алгоритм возвращается. (Раздел 9.3.1 в спецификации гласит, что MV [математическое значение] StringNumericLiteral ::: [empty] равно 0. Другими словами, числовое значение пустой строки равно 0.)
  2. Выполняется шаг 1.c.iii, который сравнивает 0 с 0 и возвращает true (и раскручивает рекурсию).

Следующие шаги показывают, как JavaScript использует алгоритм сравнения абстрактного равенства, чтобы получить значение true 0 == '0' :

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