Что такое преобразование типов?

Каждая операция (функция, оператор и т. п.) ожидают, что их входящие параметры будут иметь определенный тип. Если тип параметра не соответствует ожидаемому, есть два варианта развития событий:

  • явное приведение типов к нужным. Например, для умножения чисел, записанных в виде строк:
  •  неявное преобразование:

    Это и называется приведением типов (coercion).

Изначально в JavaScript не было исключений, поэтому для большинства операций язык использует принудительное неявное приведение типов и ошибочные значения:

Тем не менее есть некоторые кейсы (особенно при использовании новых фич), в которых выбрасывается исключение, если тип аргумента не соответствует ожидаемому:

  • Доступ к свойствам для null и undefined
  • Использование символов
  • Смешивание в одной операции обычных чисел и bigint
  • Вызов в качестве функции (или через new) значений, которые не поддерживают подобной функциональности
  • Изменение read-only свойств (в strict mode)

Как работает внутреннее преобразование типов в спецификации

В этом разделе описаны самые важные внутренние функции преобразований, определенные в спецификации ECMAScript.

Например, в TypeScript вы можете написать так:

По спецификации это будет выглядеть примерно так:

 

Преобразование к примитивам и объектам

Где бы ни ожидались примитивные значения или объекты, для их преобразования используются следующие операции:

  • ToBoolean()
  • ToNumber()
  • ToBigInt()
  • ToString()
  • ToObject()

В JavaScript есть аналоги для них:

Из-за появления нового типа bigint в дополнение к number, спецификация также определяет операцию ToNumeric(), которая должна использоваться там, где раньше была ToNumber().

Преобразование к числовым типам

  • ToNumeric() используется там, где ожидается либо число (number), либо bigint.
  • ToInteger(x) используется для чисел без дробной части.
    • Используется операцияToNumber(x), а затем удаляется дробная часть (похоже на Math.trunc()).
  • ToInt32(), ToUint32() приводят числа к 32-битовому целому формату. Используются побитовыми операторами (см. таблицу ниже).
    • ToInt32(): число со знаком в интервале [−231, 231−1]
    • ToUint32(): число без знака (unsigned) в интервале [0, 231−1]

Приведение типов в побитовых операторах для чисел (для типа Number). Для BigInt количество битов не ограничивается.

Операция Левый операнд Правый операнд Результат
<< ToInt32() ToUint32() Int32
signed >> ToInt32() ToUint32() Int32
unsigned >>> ToInt32() ToUint32() Uint32
&^| ToInt32() ToUint32() Int32
~ ToInt32() Int32

Преобразование к ключам свойств

ОперацияToPropertyKey() возвращает строку или символ. Она используется в следующих местах:

  • Оператор квадратных скобок []
  • Вычисляемые ключи свойств в литералах объекта
  • Левый операнд оператора in
  • Object.defineProperty(_, P, _)
  • Object.fromEntries()
  • Object.getOwnPropertyDescriptor()
  • Object.prototype.hasOwnProperty()
  • Object.prototype.propertyIsEnumerable()
  • Некоторые методы Reflect

Преобразование к индексам массива

  • ToLength() используется (напрямую) в основном  для строковых индексов.
    • Является вспомогательной функцией для ToIndex().
    • Диапазон результатов l: 0 ≤ l ≤ 253−1
  • ToIndex() используется для индексов TypedArray.
    • Основное отличие от ToLength(): выбрасывает исключение, если значение аргумента находится вне диапазона.
    • Диапазон результатов: i: 0 ≤ i ≤ 253−1
  • ToUint32() используется для индексов массивов.
    • Диапазон результатов i: 0 ≤ i < 232−1 (верхняя граница исключается, чтобы оставить место для .length).

Преобразование к элементам Typed Array

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

  • ToInt8()
  • ToUint8()
  • ToUint8Clamp()
  • ToInt16()
  • ToUint16()
  • ToInt32()
  • ToUint32()
  • ToBigInt64()
  • ToBigUint64()

Выражение алгоритмов спецификации в JavaScript

Дальше будет приведено несколько алгоритмов из спецификации, реализованных на JavaScript.

  • Спецификация: If Type(value) is String
    • JavaScript: if (TypeOf(value) === 'string') (описание функции ниже)
  • Спецификация: If IsCallable(method) is true
    • JavaScript: if (IsCallable(method)) (описание функции ниже)
  • Спецификация: Let numValue be ToNumber(value)
    • JavaScript: let numValue = Number(value)
  • Спецификация: Let isArray be IsArray(0)
    • JavaScript: let isArray = Array.isArray(O)
  • Спецификация: If O has a [[NumberData]] internal slot
    • JavaScript: if ('__NumberData__' in O)
  • Спецификация: Let tag be Get(O, @@toStringTag)
    • JavaScript: let tag = O[Symbol.toStringTag]
  • Спецификация: Return the string-concatenation of «[object», tag, and «]»
    • JavaScript: return '[object ' + tag + ']'

Некоторые вещи опущены, например, условные обозначения ReturnIfAbrupt ? и !.

 

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

ToPrimitive()

Операция ToPrimitive() — это промежуточный шаг для многих алгоритмов преобразования (некоторые из которых мы позже рассмотрим). Она конвертирует произвольные значения в примитивные.

ToPrimitive() часто используется в спецификации, как как многие операции работают только с примитивными значениями. Например, мы можем использовать оператор плюс (+) для сложения чисел (number) или конкатенации строк (string), но не для конкатенации массивов (Array).

JavaScript-версия ToPrimitive() выглядит примерно так:

Параметр hint может принимать одно из трех значений:

  • ‘number’ — если возможно, input должен быть конвертирован в число
  • ‘string’ — если возможно, input должен быть конвертирован в строку
  • ‘default’ — нет предпочтений, во что конвертировать

ToPrimitive() позволяет объектам переопределять способ преобразования в примитив с помощью Symbol.toPrimitive. Если объект не использует переопределение, будет применен метод OrdinaryToPrimitive():

Методы, используемые в операциях ToPrimitive() и OrdinaryToPrimitive()

Для преобразования в примитивные значения используются три метода объектов:

  • toString(), если параметр hint указывает, что результат предпочтительно должен быть строкой.
  • valueOf() — если мы хотели бы получить число.
  • Symbol.toPrimitive — для кастомного преобразования. В стандартной библиотеке этот метод используется дважды:
    • Symbol.prototype[@@toPrimitive](hint). Возвращает обернутый символ. Существует для поддержки двух кейсов: у символов есть метод toString(), который возвращает строку. Экземпляры Symbol не должны быть случайно преобразованы в строки.
    • Date.prototype[@@toPrimitive](hint). Подробно разбирается чуть ниже.

Теперь посмотрим, как параметр hint влияет на выбор того или иного метода:

  • Number() вызывает операцию ToPrimitive() с параметром hint, равным 'number':
  • String() вызывает операцию ToPrimitive() с параметром hint, равным 'string':

Как разные операции устанавливают значение hint в ToPrimitive()?

Вот несколько примеров вызова операции ToPrimitive() другими операциями с разными значениями hint:

  • hint === 'number'. Работать с числами предпочитают следующие операции:
    • ToNumeric()
    • ToNumber()
    • ToBigInt(), BigInt()
    • Абстрактное сравнение (<)
  • hint === 'string'. Эти операции предпочитают строки:
    • ToString()
    • ToPropertyKey()
  • hint === 'default'. А этим операциями все равно, какого типа значение вернется:
    • Нестрогое равенство (==)
    • Оператор сложения (+)
    • new Date(value)value может быть и строкой, и числом.

Значение по умолчанию (default) обрабатывается, как если бы оно было равно 'number'. Только Symbol и Date переопределяют это поведение.

Date.prototype[Symbol.toPrimitive]()

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

Единственное отличие от дефолтного алгоритма состоит в том, что значение 'default' интерпретируется как 'string' (а не как 'number'). В этом можно убедиться, если использовать операции, которые устанавливают hint в значение 'default':

  • == приводит дату к строке, если второй операнд является примитивом, отличным от undefined, null или boolean:
  • Даже если первый операнд оператора + является числом, результат — все равно строка. Это значит, что дата приводится к строке, и происходит конкатенация:

ToString() и связанные операции

JavaScript-версия операции ToString() выглядит так:

Обратите внимание, что здесь используется операция ToPrimitive() — для получения от объекта промежуточного примитивного значения, которое будет конвертировано в строку.

ToString() интересным образом отличается от работы функции String(). Если argument является символом, то первая выбрасывает TypeError, а вторая — нет. Так происходит, потому что по умолчанию конвертирование символов в строку приводит к исключению:

String() и Symbol.prototype.toString() переопределяют это поведение (обе функции описаны далее в статье):

String()

Мы видим, что String() ведет себя по-разному, если вызывается как функция и если вызывается как конструктор (new).

Вспомогательные функции StringCreate() и SymbolDescriptiveString() выглядят следующим образом:

Symbol.prototype.toString()

В дополнение к String(), вы можете использовать метод .toString() для конвертации символов в строку. Его спецификация выглядит следующим образом:

Object.prototype.toString()

Вот так выглядит дефолтная спецификация метода .toString():

Эта операция используется, если вы конвертируете в строку обычные объекты:

По умолчанию она также применяется для конвертации инстансов классов:

Вы можете изменять, что следует за словом object в квадратных скобках:

Если вы напрямую вызываете Object.prototype.toString, вы можете получить доступ к переопределенному поведению:

ToPropertyKey()

Операция ToPropertyKey() среди прочего используется оператором квадратных скобок ([]):

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

ToNumeric() и связанные операции

Операция ToNumeric() среди прочего используется оператором умножения (*).

ToNumber()

Операция ToNumber() выглядит следующим образом:

Структура ToNumber() очень напоминает структуру ToString().

Операции с преобразованием типов

Оператор сложения (+)

Вот так описан в спецификации JavaScript оператор сложения:

Шаги этого алгоритма:

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

Нестрогое равенство (==)

Следующие вспомогательные операции в статье не описаны:

  • strictEqualityComparison()
  • StringToBigInt()
  • isSameMathematicalValue()

Глоссарий

Разобравшись немного с приведением типов в JavaScript, давайте составим небольшое резюме в виде словарика терминов:

  • При преобразовании типов (type conversion) мы хотим, чтобы выходное значение имело некоторый заданный тип. Если входное значение уже имеет этот тип, то оно просто возвращается без изменений. В противном случае, оно будет сконвертировано по определенным правилам к желаемому типу.
  • Явное преобразование типов (explicit type conversion) означает, что программист использует операцию (функцию или оператор) для запуска преобразования типов. Явное преобразование может быть:
    • Проверенным (Checked): Если преобразование невозможно, выбрасывается исключение.
    • Непроверенным (Unchecked): Если преобразование невозможно, возвращается ошибка.
  • Приведение типов (type coercion) — это неявное преобразование типов. Операции автоматически преобразуют свои аргументы в те типы, которые им нужны. Такое преобразование может быть и проверенным, и непроверенным.
Оригинал: Type coercion in JavaScript, by Dr. Axel Rauschmayer

0 комментариев

Оставить комментарий

*Доступные HTML-теги: a, abbr, blockquote, code, pre, del, i, em, strong, b, strike
*Не будет опубликован