image Каким бы опытом программирования на JavaScript вы ни обладали, скорее всего, вы не понимаете язык в полной мере. Это лаконичное руководство исследует типы более глубоко, чем все существующие книги: вы узнаете, как работают типы, о проблемах их преобразования и научитесь пользоваться новыми возможностями.

Как и в других книгах серии «Вы не знаете JS», здесь показаны нетривиальные аспекты языка, от которых программисты JavaScript предпочитают держаться подальше (или полагают, что они не существуют). Вооружившись этими знаниями, вы достигнете истинного мастерства JavaScript.

Отрывок. Равенство строгое и нестрогое.


Нестрогое равенство проверяется оператором ==, а строгое — оператором ===. Оба оператора используются для сравнения двух значений на «равенство», но выбор формы (строгое/нестрогое) приводит к очень важным различиям в поведении, особенно в том, как принимается решение о равенстве.

По поводу этих двух операторов существует распространенное заблуждение: «== проверяет на равенство значения, а === проверяет на равенство как значения, так и типы». Звучит разумно,
но неточно. В бесчисленных авторитетных книгах и блогах, посвященных JavaScript, говорится именно это, но, к сожалению, все они ошибаются.

Правильное описание выглядит так: «== допускает преобразование типа при проверке равенства, а === запрещает преобразование типа».

Быстродействие проверки равенства


Остановитесь и подумайте, чем первое (неточное) объяснение отличается от второго (точного).
В первом объяснении кажется очевидным, что оператор === выполняет больше работы, чем ==, потому что он также должен проверить тип.

Во втором объяснении оператор == выполняет больше работы, потому что при различных типах ему приходится проходить через преобразование типа.

Не попадайтесь в ловушку, в которую попадают многие. Не думайте, что это хоть как-то отразится на быстродействии программы, а == будет сколько-нибудь ощутимо медленнее ===. Хотя преобразование занимает какое-то время, оно занимает считаные микросекунды (да, миллионные доли секунды).

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

Если вы сравниваете два значения разных типов, быстродействие не является важным фактором. Вы должны спрашивать себя о другом: если я сравниваю два значения, хочу ли я, чтобы выполнялось преобразование типов или нет?

Если преобразование вам нужно, используйте нестрогое равенство ==, а если преобразование нежелательно, то используйте строгое равенство ===.

Оба оператора, == и ===, проверяют типы своих операндов. Различие в том, как они реагируют на несовпадение типов.

Абстрактная проверка равенства


Поведение оператора == определяется в разделе 11.9.3 спецификации ES5 («Алгоритм абстрактной проверки равенства»). Здесь приведен подробный, но простой алгоритм, с явным перечислением всех возможных комбинаций типов и способов преобразований типов (при необходимости), которые должны применяться в каждой комбинации.

Когда кто-то осуждает (неявное) преобразование типа как слишком сложное и содержащее слишком много дефектов для полезного практического применения, он осуждает именно правила «абстрактной проверки равенства». Обычно говорят, что этот механизм слишком сложен и противоестественен для практического изучения и использования, и что он скорее создает в JS-программах ошибки, чем упрощает чтение кода.

Я считаю, что это ошибочное предположение — ведь вы, читатели, являетесь компетентными разработчиками, которые пишут алгоритмы, то есть код (а также читают и разбираются в нем), целыми днями напролет. По этой причине я постараюсь объяснить «абстрактную проверку равенства» простыми словами. Однако я также рекомендую прочитать раздел 11.9.3 спецификации ES5. Думаю, вас удивит, насколько там все логично.

По сути, первый раздел (11.9.3.1) утверждает, что, если два сравниваемых значения относятся к одному типу, они сравниваются простым и естественным способом. Например, 42 равно только 42, а строка «abc» равна только «abc».

Несколько второстепенных исключений, о которых следует помнить:

  • Значение NaN никогда не равно само себе (см. главу 2).
  • ??+0 и -0 равны друг другу (см. главу 2).

Последняя секция в разделе 11.9.3.1 посвящена нестрогой проверке равенства == с объектами (включая функции и массивы). Два таких значения равны только в том случае, если оба они ссылаются в точности на одно значение. Никакое преобразование типа при этом не выполняется.

Строгая проверка равенства === определяется идентично 11.9.3.1, включая положение о двух объектных значениях. Этот факт очень малоизвестен, но == и === при сравнении двух объектов ведут себя полностью идентично!

Оставшаяся часть алгоритма в 11.9.3 указывает, что нестрогое равенство == может использоваться для сравнения двух разнотипных значений, одно или оба из которых потребуют
неявного преобразования. В результате преобразования обазначения приводятся к одному типу, после чего они могут напрямую сравниваться на равенство по простой идентичности
значений.

Операция нестрогой проверки неравенства != определяется в точности так, как следовало ожидать; по сути операция == выполняется в полной мере, с последующим вычислением
отрицания результата. То же относится к операции строгой проверки неравенства !==.

Сравнение: строки и числа


Для демонстрации преобразования == сначала создадим примеры строки и числа, что было сделано ранее в этой главе:

var a = 42;
var b = "42";

a === b;     // false
a == b;       // true

Как и следовало ожидать, проверка a === b завершается неудачей, поскольку преобразование не разрешено, а значения 42 и «42» различны.

Однако во втором сравнении a == b используется нестрогое равенство; это означает, что если типы окажутся различными, алгоритм сравнения выполнит неявное преобразование одного
или обоих значений.

Но какое именно преобразование здесь выполняется? Станет ли значение a, то есть 42, строкой, или же значение b «42» станет числом? В спецификации ES5 в разделах 11.9.3.4–5 говорится:

  1. Если Type(x) относится к типу Number, а Type(y) относится к типу String, вернуть результат сравнения x == ToNumber(y).
  2. Если Type(x) относится к типу String, а Type(y) относится к типу Number, вернуть результат сравнения ToNumber(x) == y.

В спецификации используются формальные имена типов Number и String, тогда как в книге для примитивных типов обычно используются обозначения number и string. Не путайте регистр символа Number в спецификации со встроенной функцией Number(). Для наших целей регистр символов в имени типа роли не играет — они означают одно и то же.

Спецификация говорит, что значение «42» преобразуется в число для сравнения. О том, как именно выполняется преобразование, уже было рассказано ранее, а конкретно при описании абстрактной операции ToNumber. В этом случае вполне очевидно,
что полученные два значения 42 равны.

Сравнение: что угодно с логическими значениями


Одна из самых опасных ловушек при неявном преобразовании типа == встречается при попытке прямого сравнения значения с true или false.

Пример:

var a = "42";
var b = true;

a == b; // false

Погодите, что здесь происходит? Мы знаем, что «42» является истинным значением (см. ранее в этой главе). Как же получается, что сравнение его с true оператором нестрогого равенства ==
не дает true?

Причина проста и одновременно обманчиво хитроумна. Ее легко понять неправильно, многие разработчики JS не прикладывают должных усилий, чтобы полностью разобраться в ней.

Еще раз процитируем спецификацию, разделы 11.9.3.6–7:

  1. Если Type(x) относится к типу Boolean, вернуть результат сравнения ToNumber(x) == y.
  2. Если Type(y) относится к типу Boolean, вернуть результат сравнения x == ToNumber(y).

Посмотрим, что здесь. Первый шаг:

var x = true;
var y = "42";

x == y; // false

Type(x) действительно относится к типу Boolean, поэтому выполняется операция ToNumber(x), которая преобразует true в 1. Теперь вычисляется условие 1 == «42». Типы все равно различны, поэтому (фактически рекурсивно) алгоритм повторяется; как и в предыдущем случае, «42» преобразуется в 42, а условие 1 == 42 очевидно ложно.

Если поменять операнды местами, результат останется прежним:

var x = "42";
var y = false;

x == y; // false

На этот раз Type(y) имеет тип Boolean, так что ToNumber(y) дает 0. Условие «42» == 0 рекурсивно превращается в 42 == 0, что, разумеется, ложно.

Другими словами, значение «42» ни == true, ни == false. На первый взгляд это утверждение кажется совершенно немыслимым. Как значение может быть ни истинным, ни ложным?

Но в этом и заключается проблема! Вы задаете совершенно не тот вопрос. Хотя на самом деле это не ваша вина, это вас обманывает мозг.

Значение «42» действительно истинно, но конструкция «42» == true вообще не выполняет проверку boolean/преобразование, что бы там ни говорил ваш мозг. «42» не преобразуется в boolean (true); вместо этого true преобразуется в 1, а затем «42» преобразуется в 42.

Нравится вам это или нет, ToBoolean здесь вообще не используется, так что истинность или ложность «42» вообще не важна для операции ==! Важно понимать, как алгоритм сравнения == ведет себя во всех разных комбинациях типов. Если с одной из сторон стоит логическое значение boolean, то оно всегда сначала преобразуется в число.

Если это кажется вам странным, вы не одиноки. Лично я рекомендую никогда, никогда, ни при каких обстоятельствах не использовать == true или == false. Никогда.

Но помните, что я здесь говорю только о ==. Конструкции === true и === false не допускают преобразование типа, поэтому они защищены от скрытого преобразования ToNumber.

Пример:

var a = "42";

// плохо (проверка не проходит!):
if (a == true) {
    // ..
}

// тоже плохо (проверка не проходит!):
if (a === true) {
    // ..
}

// достаточно хорошо (неявное преобразование):
if (a) {
   // ..
}

// лучше (явное преобразование):
if (!!a) {
    // ..
}

// тоже хорошо (явное преобразование):
if (Boolean( a )) {
    // ..
}

Если вы будете избегать == true или == false (нестрогое равенство с boolean) в своем коде, вам никогда не придется беспокоиться об этой ловушке, связанной с истинностью/ложностью.

Сравнение: null с undefined


Другой пример неявного преобразования встречается при использовании нестрогой проверки равенства == между значениями null и undefined. Снова процитирую спецификацию ES5,
разделы 11.9.3.2–3:

  1. Если x содержит null, а y содержит undefined, вернуть true.
  2. Если x содержит undefined, а y содержит null, вернуть true.

Null и undefined при сравнении нестрогим оператором == равны друг другу (то есть преобразуются друг к другу), и никаким другим значениям во всем языке.

Для нас это означает то, что null и undefined могут рассматриваться как неразличимые для целей сравнения, если вы используете нестрогий оператор проверки равенства ==, разрешающий их взаимное неявное преобразование:

var a = null;
var b;

a == b; // true
a == null; // true
b == null; // true

a == false; // false
b == false; // false
a == ""; // false
b == ""; // false
a == 0; // false
b == 0; // false

Преобразование между null и undefined безопасно и предсказуемо, и никакие другие значения не могут дать ложные положительные срабатывания при такой проверке. Я рекомендую использовать это преобразование, чтобы null и undefined не различались в программе и интерпретировались как одно значение.

Пример:

var a = doSomething();

if (a == null) {
 // ..
}

Проверка a == null проходит только в том случае, если doSomething() вернет null или undefined, и не пройдет при любом другом значении (включая 0, false и "").

Явная форма этой проверки, которая запрещает любые подобные преобразования типов, выглядит (на мой взгляд) намного уродливее и, возможно, работает чуть менее эффективно!

var a = doSomething();

if (a === undefined || a === null) {
 // ..
}

Я считаю, что форма a == null — еще один пример ситуации, в которой неявное преобразование упрощает чтение кода, но делает это надежно и безопасно.

Сравнение: объекты и необъекты


Если объект/функция/массив сравнивается с простым скалярным примитивом (string, number или boolean), в спецификации ES5 говорится следующее (раздел 11.9.3.8–9):

  1. Если Type(x) относится к типу String или Number, а Type(y) относится к типу Object, вернуть результат сравнения x == ToPrimitive(y).
  2. Если Type(x) относится к типу Object, а Type(y) относится к типу String или Number, вернуть результат сравнения ToPrimitive(x) == y.

Возможно, вы заметили, что в этих разделах спецификации упоминаются только String и Number, но не Boolean. Дело в том, что, как упоминалось выше, разделы 11.9.3.6–7 гарантируют, что любой операнд Boolean был сначала представлен в виде Number.

Пример:

var a = 42;
var b = [ 42 ];

a == b; // true

Для значения [ 42 ] вызывается абстрактная операция ToPrimitive (см. «Абстрактные операции»), которая дает результат «42». С этого момента остается простое условие «42» == 42, которое, как мы уже выяснили, превращается в 42 == 42, так что a и b равны с точностью до преобразования типа.

Как и следовало ожидать, все особенности абстрактной операции ToPrimitive, которые рассматривались ранее в этой главе ((toString(), valueOf()), применимы и в данном случае. Это может быть весьма полезно, если у вас имеется сложная структура данных и вы хотите определить для нее специализированный метод valueOf(), который должен будет предоставлять простое значение для целей проверки равенства.

В главе 3 рассматривалась «распаковка» объектной обертки вокруг примитивного значения (как в new String(«abc»), например), в результате чего возвращается нижележащее примитивное
значение («abc»). Это поведение связано с преобразованием ToPrimitive в алгоритме ==:

var a = "abc";
var b = Object( a );            // то же, что `new String( a )`

a === b;                           // false
a == b;                             // true

a == b дает true, потому что b преобразуется (или «распаковывается») операцией ToPrimitive в базовое простое скалярное примитивное значение «abc», которое совпадает со значением из a.

Есть некоторые значения, для которых это не так из-за других переопределяющих правил в алгоритме ==. Пример:

var a = null;
var b = Object( a );           // то же, что `Object()`
a == b;                            // false

var c = undefined;
var d = Object( c );           // то же, что `Object()`
c == d;                            // false

var e = NaN;
var f = Object( e );           // то же, что `new Number( e )`
e == f;                            // false

Значения null и undefined не могут упаковываться (у них нет эквивалентной объектной обертки), так что Object(null) принципиально не отличается от Object(): оба вызова создают обыч-
ный объект.

NaN можно упаковать в эквивалентную объектную обертку Number, но когда == вызывает распаковку, сравнение NaN == NaN не проходит, потому что значение NaN никогда не равно самому себе (см. главу 2).

» Более подробно с книгой можно ознакомиться на сайте издательства
» Оглавление
» Отрывок

Для Хаброжителей скидка 25% по купону — JavaScript

По факту оплаты бумажной версии книги на e-mail высылается электронная книга.

Комментарии (4)


  1. Defersa
    25.06.2019 12:18
    +2

    Я не понимаю весь этот шум вокруг разного поведения JS для сравнений разных типов. Таких случаев меньше процента, мало кто сталкивается, все достаточно просто тестируется через консоль. Зачем все эти постоянные статьи на хабре? Книгу пиарить?


    1. Aingis
      25.06.2019 12:52

      Книга, кстати, хорошая, давно доступная онлайн в оригинале и даже с переводом. Если бы все её читали (или хоть какой-нибудь материал по теме), глядишь, не было бы этого страха от неизбежного приведения типов и карго-культа статической типизации.


    1. rboots
      25.06.2019 16:31

      Работал в команде с бывшими Ruby-программистами, которых заставили писать на JavaScript. Они такие книжки не читали, считали себя и так ковбоями и жутко матерились от любой очевидной особенности языка. Им прочитать книжку по диагонали очень стоило бы. А так вы правы, все правила легко проверяются.


  1. bashkadove
    25.06.2019 13:09

    Допечатайте пожалуйста первую книгу из этой серии — ES6 и не только. Справочное пособие. Ни где не могу купить