Позвольте мне заставить вас полюбить динамическую систему типизации Javascript. Недавно меня увлекла книга Кайла Симпсона Вы не знаете JS, типы и грамматику, в которой он утверждает, что динамическая типизация и гибкость, которую она обеспечивает, вопреки общепринятым представлениям и мнениям, не так проблематичны, как пока вы понимаете, как все работает под капотом. При понимании различных алгоритмов неявность, вызываемая выражениями Javascript, является не камнем преткновения, а преимуществом.

Вместо того, чтобы быть загадкой, приведение типов в Javascript подробно описано в спецификации до мельчайших деталей. На протяжении всей этой части я буду ссылаться на самую последнюю спецификацию ES2017, но я не ожидаю, что такой ключевой аспект языка, как этот, сильно изменится в предыдущих или будущих версиях. Чтобы понять, как абстрактное равенство работает в Javascript, необходимо сначала ознакомиться с набором основных алгоритмов приведения для преобразования между типами, которые описаны в спецификации. Это абстрактные алгоритмы, которые, хотя и не могут быть доступны напрямую, используются другими алгоритмами, которые предоставляют общедоступный интерфейс и, как таковые, несут ответственность за функции (или недостатки, в зависимости от вашей точки зрения), которые позволяют Javascript быть относительно кратким, а также язык высокого уровня.

Основные алгоритмы преобразования

Я возьму их в том же порядке, в котором они указаны в спецификации.

ToPrimitive

Целью этой операции является преобразование непримитивных типов (объектов) в примитивы. Он принимает подсказку, которая по умолчанию является либо «строкой», либо «числом», указывающую предпочтение того, как ввод должен быть принудительно введен. Вы можете определить свой собственный метод toPrimitive для объекта, и в этом случае алгоритм будет использовать его вместо общего алгоритма OrdinaryToPrimitive. Это позволит вам определять пользовательские подсказки и возвращать что-то отличное от числа или строки.

Конечно, если ввод уже является примитивом, алгоритм просто возвращает его.

Обычный-примитивный

В обычном алгоритме результатом преобразования объекта является либо число, либо строка (если не выдается ошибка TypeError). В зависимости от подсказки алгоритм попытается вызвать метод toValue, а затем метод toString. По умолчанию алгоритм сначала пытается преобразовать значение в число, вызывая метод toValue (намек на число), и, если результат не является объектом, пытается использовать метод toString. Обратный порядок используется с учетом подсказки String.

ToBoolean

ToBoolean — один из самых простых алгоритмов преобразования, который, по сути, возвращает false, если вход находится в списке из 6 «ложных» значений. В противном случае он возвращает true. Вот список:

  1. Неопределенный
  2. Нулевой
  3. Ложь
  4. +0 or -0
  5. NaN
  6. "" (пустой строки)

ToNumber

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

  1. Undefined -> NaN (может показаться неожиданным)
  2. Нуль -> 0
  3. правда -> 1, ложь -> 0
  4. Номер -> Номер
  5. Символ -> Ошибка

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

Если ввод является Object, он вызывает ToPrimitive с подсказкой Number, а затем рекурсивно вызывает сам себя.

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

  • ToInteger
  • ToInt32
  • ToUint32
  • ToInt16
  • ToUint16
  • ToInt8
  • ToUint8
  • ToUint8Зажим

ТоСтрока

ToString очень похож на ToNumber. Он имеет несколько основных сопоставлений:

  1. неопределенный -> «неопределенный»
  2. ноль ноль"
  3. истина -> «истина», ложь -> «ложь»
  4. Строка -> Строка
  5. Символ -> Ошибка

Если ввод является Object, на этот раз вызывает ToPrimitive с намеком на String и рекурсивно вызывает сам себя.

Если ввод является числом, верните NumberToString ниже.

ЧислоСтроки

NumberToString — это специальный алгоритм преобразования чисел в строки. Он подчиняется следующим шагам:

  1. NaN -> «NaN»
  2. +0, -0 -> “0”
  3. Если ввод отрицательный, верните конкатенацию «-» с рекурсивным вызовом самого себя на -вводе.
  4. Если ввод равен бесконечности, вернуть «Бесконечность».
  5. В противном случае он в значительной степени просто печатает число, как и следовало ожидать, с оговоркой, что числа с показателем степени выше 20 (см. мой предыдущий пост о представлениях с плавающей запятой в Javascript) представлены в усеченном виде, мало чем отличающемся от научной нотации, которую вы, возможно, помните. из средней школы (например, 6.0221408569999995e+23).

Объект

Наконец, у нас есть ToObject, который, по сути, вызывает различные конструкторы для нативных типов и устанавливает их свойство value равным входному аргументу. Ни Undefined, ни Null не могут быть преобразованы в объекты и вызовут исключение TypeError.

Определение равенства

Теперь, поняв, что существует набор абстрактных алгоритмов, которые Javascript использует для различных преобразований, мы теперь можем понять операторы сравнения и их механику. Зная, что они делают, можно избежать распространенных ошибок при их использовании, а также узнать, как использовать их причуды на службе более читаемого кода.

На протяжении всей этой части я сосредоточусь на алгоритмах равенства: как на абстрактном, так и на строгом равенстве, как определено в спецификации.

ECMAScript определяет абстрактное сравнение равенства — по сути, двойное равенство. Под капотом это делает попытку преобразовать один или несколько аргументов, если они не одного типа. Если он может получить два аргумента одного и того же типа, он выполняет сравнение строгого равенства для результата. В противном случае это ложь. Довольно просто, верно?

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

Итак, шаги следующие:

  1. Одинаковые типы: строгое равенство Прежде всего, если аргументы одного типа, не беспокойтесь о следующем, а сразу переходите к алгоритму строгого равенства.
  2. Null, Undefined: true В качестве промежуточного шага проверьте, является ли одно из них неопределенным, а другое — нулевым, и в этом случае верните значение true.
  3. Преобразование строки в число. Если один аргумент является строкой, а другой — числом, попробуйте преобразовать строку в число, вызвав ToNumber выше, а затем снова рекурсивно вызовите этот абстрактный алгоритм.
  4. Преобразовать логическое значение в числоЗатем, если любой из аргументов является логическим значением, преобразовать логическое значение в число. Итак, если у вас есть одно логическое значение и одна строка, будет промежуточный шаг от числа к строке и, наконец, от числа к числу.
  5. Преобразовать объект в примитив Наконец, если какой-либо аргумент является объектом, вызовите ToPrimitive, чтобы попытаться преобразовать его в число или строку (подсказка не предлагается, поэтому используется порядок по умолчанию: valueOf, затем toString).
  6. Неверно

Строгое равенство

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

  1. Если входные данные относятся к разным типам, сразу же возвращайте false.

2. Если аргументы Числа, то если:

а. ЛЮБОЙ аргумент равен NaN, верните false.

б. В противном случае то же числовое значение является истинным.

в. Это включает в себя +0 и -0 (имеет смысл математически, но не уверен, что это имеет смысл во всех контекстах).

3. Наконец, попробуйте SameValueNonNumber, если входные данные не являются числом.

Вы можете видеть, что странное исключение/предостережение для простоты этого правила заключается в том, что NaN не равен самому себе. Обратитесь к Вы не знаете JS, Глава 2 для объяснения.

ТожеЗначениеНеЧисло

Наконец, используйте этот алгоритм для сравнения нечисел:

  1. Undefined и Null равны сами себе
  2. Если входные данные являются строками, убедитесь, что каждая цифра в кодовой последовательности одинакова и имеет одинаковую длину.
  3. Булевы равны сами себе
  4. Символ равен тому же Символу
  5. Объекты должны быть равны в соответствии со ссылкой или «значением объекта». Это означает, что каждый ввод относится к одному и тому же адресу в памяти, к одной и той же структуре. Стоит отметить, что объекты, такие как конструкторы, такие как Number() или String(), будут возвращать здесь примитивы, поскольку их valueOf ссылается на значение, которое объект предназначен для переноса.

Равенство на практике

Давайте рассмотрим пару примеров, чтобы увидеть, как это работает на практике. Теперь мы можем видеть промежуточные шаги для сравнений, таких как 0 == «», 0 == «0» и true == «1». Это произвольные примеры, и их еще много, но я просто хочу показать, насколько возможно понять, какие результаты этих преобразований дадут понимание лежащей в их основе механики.

0 == “”

Давайте просто разберем это шаг за шагом, следуя алгоритмам, изложенным выше. Мы видим, что шаги 1 и 2 неприменимы, но применимы к шагу 3, и в этом случае мы хотим привести «» к числу. В соответствии с семантикой ToNumber пустая строка преобразуется в 0. Итак, теперь у нас остается 0 == 0. Вызов Strict Equals для 0 и 0 верен, учитывая, что ни одно из значений не является NaN и что знак нуля равен не имеющий отношения.

0 == “0”

Опять же, у нас есть одно число и одна строка; следовательно, мы преобразуем второй аргумент в число с помощью ToNumber. «0» становится 0 в соответствии с этим алгоритмом. И снова верно строгое равенство на 0 === 0.

true == "1"

На этот раз мы пропускаем весь путь к шагу 4, преобразуя true в 1. Затем мы конвертируем «1» в 1 и, наконец, строго сравниваем результат, возвращая true.

Давайте также взглянем на некоторые примеры с объектами, чтобы выделить пару сложностей.

Если вы сравниваете объекты с той же формой, но разными ссылками, например, при объявлении двух литералов объекта, они будут иметь разные значения равенства. Например, {} === {} является ложным.

Но если объект имеет другой тип возвращаемого значения для его метода valueOf, вы можете получить неожиданное абстрактное равенство между ним и некоторым примитивом. Но поскольку аргументы одного и того же типа зависят от алгоритма строгого равенства, объекты ссылки на различие будут возвращать false в этом случае независимо от возвращаемого значения метода valueOf.

const MyExoticConstructor = function() {};

MyExoticConstructor.prototype.valueOf = function() { return 1; };

const myInstance = новый MyExoticConstructor();

console.log(мой экземпляр == 1); //истинный

console.log(мой экземпляр === 1); //ложный

const myOtherInstance = новый MyExoticConstructor();

console.log(myInstance == myOtherInstance); //ложный

Вывод

Итак, это все, что я могу сказать на данный момент о приведении типов и сравнениях на равенство. В этой области есть еще темы для изучения, такие как реляционные сравнения и другие тонкие приведения, которые происходят более или менее явно. Опять же, я бы отослал вас к You Don’t Know JS для отличного введения в такие темы, не вдаваясь в детали.

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

Кто знал, что можно так много сказать об одном маленьком символе в языке программирования? Но хотя может быть много шагов, каждый шаг понятен, а также целостно понятен, если немного попрактиковаться в некоторых тестовых примерах. В заключение, вы можете считать значения равными в заранее определенных, а иногда и произвольных пределах!

Ссылки и дополнительная литература

  1. Спецификация Javascript http://www.ecma-international.org/ecma-262/8.0/
  2. Вы не знаете JS, Кайл Симпсон https://github.com/getify/You-Dont-Know-JS/tree/master/types%20%26%20grammar
  3. Документы MDN, Равенство и одинаковость https://developer.mozilla.org/en-US/docs/Web/JavaScript/Equality_comparisons_and_sameness