Что такое преобразование типов?
Каждая операция (функция, оператор и т. п.) ожидают, что их входящие параметры будут иметь определенный тип. Если тип параметра не соответствует ожидаемому, есть два варианта развития событий:
- явное приведение типов к нужным. Например, для умножения чисел, записанных в виде строк:
Number('3') * Number('2') // 6
- неявное преобразование:
'3' * '2' // 6
Это и называется приведением типов (coercion).

Изначально в JavaScript не было исключений, поэтому для большинства операций язык использует принудительное неявное приведение типов и ошибочные значения:
// Coercion
assert.equal(3 / true, 3);
// Error values
assert.equal(1 / 0, Infinity);
assert.equal(Number('xyz'), NaN);
Тем не менее есть некоторые кейсы (особенно при использовании новых фич), в которых выбрасывается исключение, если тип аргумента не соответствует ожидаемому:
- Доступ к свойствам для
null
иundefined
undefined.prop
// TypeError: Cannot read property 'prop' of undefined
null.prop
// TypeError: Cannot read property 'prop' of null
'prop' in null
// TypeError: Cannot use 'in' operator to search for 'prop' in null
- Использование символов
6 / Symbol()
// TypeError: Cannot convert a Symbol value to a number
- Смешивание в одной операции обычных чисел и
bigint
6 / 3n
// TypeError: Cannot mix BigInt and other types, use explicit conversions
- Вызов в качестве функции (или через
new
) значений, которые не поддерживают подобной функциональности
123()
// TypeError: 123 is not a function
(class {})()
// TypeError: Class constructor cannot be invoked without 'new'
new 123
// TypeError: 123 is not a constructor
new (() => {})
// TypeError: (intermediate value) is not a constructor
- Изменение read-only свойств (в strict mode)
'abc'.length = 1
// TypeError: Cannot assign to read only property 'length' of string 'abc'
Object.freeze({prop:3}).prop = 1
// TypeError: Cannot assign to read only property 'prop' of object '#<Object>'
Как работает внутреннее преобразование типов в спецификации
В этом разделе описаны самые важные внутренние функции преобразований, определенные в спецификации ECMAScript.
Например, в TypeScript вы можете написать так:
function multiply(leftValue: number, rightValue: number) {
// ···
}
По спецификации это будет выглядеть примерно так:
function multiply(leftValue, rightValue) {
let lnum = ToNumeric(leftValue);
let rnum = ToNumeric(rightValue);
// ···
}
Преобразование к примитивам и объектам
Где бы ни ожидались примитивные значения или объекты, для их преобразования используются следующие операции:
ToBoolean()
ToNumber()
ToBigInt()
ToString()
ToObject()
В JavaScript есть аналоги для них:
Boolean(0)
// false
Boolean(1)
// true
Number('123')
// 123
Из-за появления нового типа 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')
(описание функции ниже)
- JavaScript:
- Спецификация: If IsCallable(method) is true
- JavaScript:
if (IsCallable(method))
(описание функции ниже)
- JavaScript:
- Спецификация: Let numValue be ToNumber(value)
- JavaScript:
let numValue = Number(value)
- JavaScript:
- Спецификация: Let isArray be IsArray(0)
- JavaScript:
let isArray = Array.isArray(O)
- JavaScript:
- Спецификация: If O has a [[NumberData]] internal slot
- JavaScript:
if ('__NumberData__' in O)
- JavaScript:
- Спецификация: Let tag be Get(O, @@toStringTag)
- JavaScript:
let tag = O[Symbol.toStringTag]
- JavaScript:
- Спецификация: Return the string-concatenation of «[object», tag, and «]»
- JavaScript:
return '[object ' + tag + ']'
- JavaScript:
Некоторые вещи опущены, например, условные обозначения ReturnIfAbrupt ? и !.
// Улучшенная версия typeof
function TypeOf(value) {
const result = typeof value;
switch (result) {
case 'function':
return 'object';
case 'object':
if (value === null) {
return 'null';
} else {
return 'object';
}
default:
return result;
}
}
function IsCallable(x) {
return typeof x === 'function';
}
Примеры алгоритмов преобразования
ToPrimitive()
Операция ToPrimitive()
— это промежуточный шаг для многих алгоритмов преобразования (некоторые из которых мы позже рассмотрим). Она конвертирует произвольные значения в примитивные.
ToPrimitive()
часто используется в спецификации, как как многие операции работают только с примитивными значениями. Например, мы можем использовать оператор плюс (+
) для сложения чисел (number) или конкатенации строк (string), но не для конкатенации массивов (Array).
JavaScript-версия ToPrimitive()
выглядит примерно так:
function ToPrimitive(input: any,
hint: 'default' | 'string' | 'number' = 'default') {
if (TypeOf(input) === 'object') {
let exoticToPrim = input[Symbol.toPrimitive]; // (A)
if (exoticToPrim !== undefined) {
let result = exoticToPrim.call(input, hint);
if (TypeOf(result) !== 'object') {
return result;
}
throw new TypeError();
}
if (hint === 'default') {
hint = 'number';
}
return OrdinaryToPrimitive(input, hint);
} else {
// input is already primitive
return input;
}
}
Параметр hint
может принимать одно из трех значений:
- ‘number’ — если возможно,
input
должен быть конвертирован в число - ‘string’ — если возможно,
input
должен быть конвертирован в строку - ‘default’ — нет предпочтений, во что конвертировать
ToPrimitive()
позволяет объектам переопределять способ преобразования в примитив с помощью Symbol.toPrimitive
. Если объект не использует переопределение, будет применен метод OrdinaryToPrimitive()
:
function OrdinaryToPrimitive(O: object, hint: 'string' | 'number') {
let methodNames;
if (hint === 'string') {
methodNames = ['toString', 'valueOf'];
} else {
methodNames = ['valueOf', 'toString'];
}
for (let name of methodNames) {
let method = O[name];
if (IsCallable(method)) {
let result = method.call(O);
if (TypeOf(result) !== 'object') {
return result;
}
}
}
throw new TypeError();
}
Методы, используемые в операциях ToPrimitive() и OrdinaryToPrimitive()
Для преобразования в примитивные значения используются три метода объектов:
toString()
, если параметрhint
указывает, что результат предпочтительно должен быть строкой.valueOf()
— если мы хотели бы получить число.Symbol.toPrimitive
— для кастомного преобразования. В стандартной библиотеке этот метод используется дважды:Symbol.prototype[@@toPrimitive](hint)
. Возвращает обернутый символ. Существует для поддержки двух кейсов: у символов есть методtoString()
, который возвращает строку. Экземпляры Symbol не должны быть случайно преобразованы в строки.Date.prototype[@@toPrimitive](hint)
. Подробно разбирается чуть ниже.
Теперь посмотрим, как параметр hint
влияет на выбор того или иного метода:
Number()
вызывает операциюToPrimitive()
с параметромhint
, равным'number'
:> Number({valueOf() {return 1}, toString() {return 'a'}}) 1
String()
вызывает операциюToPrimitive()
с параметромhint
, равным'string'
:String({valueOf() {return 1}, toString() {return 'a'}}) 'a'
Как разные операции устанавливают значение 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]()
Даты приводятся к примитивному значению следующим образом:
Date.prototype[Symbol.toPrimitive] = function (
hint: 'default' | 'string' | 'number') {
let O = this;
if (TypeOf(O) !== 'object') {
throw new TypeError();
}
let tryFirst;
if (hint === 'string' || hint === 'default') {
tryFirst = 'string';
} else if (hint === 'number') {
tryFirst = 'number';
} else {
throw new TypeError();
}
return OrdinaryToPrimitive(O, tryFirst);
};
Единственное отличие от дефолтного алгоритма состоит в том, что значение 'default'
интерпретируется как 'string'
(а не как 'number'
). В этом можно убедиться, если использовать операции, которые устанавливают hint
в значение 'default'
:
==
приводит дату к строке, если второй операнд является примитивом, отличным отundefined
,null
илиboolean
:
const d = new Date('2222-03-27');
d == 'Wed Mar 27 2222 01:00:00 GMT+0100 (Central European Standard Time)' // true
- Даже если первый операнд оператора
+
является числом, результат — все равно строка. Это значит, что дата приводится к строке, и происходит конкатенация:
123 + d // '123Wed Mar 27 2222 01:00:00 GMT+0100 (Central European Standard Time)'
ToString() и связанные операции
JavaScript-версия операции ToString()
выглядит так:
function ToString(argument) {
if (argument === undefined) {
return 'undefined';
} else if (argument === null) {
return 'null';
} else if (argument === true) {
return 'true';
} else if (argument === false) {
return 'false';
} else if (TypeOf(argument) === 'number') {
return Number.toString(argument);
} else if (TypeOf(argument) === 'string') {
return argument;
} else if (TypeOf(argument) === 'symbol') {
throw new TypeError();
} else if (TypeOf(argument) === 'bigint') {
return BigInt.toString(argument);
} else {
// argument is an object
let primValue = ToPrimitive(argument, 'string'); // (A)
return ToString(primValue);
}
}
Обратите внимание, что здесь используется операция ToPrimitive()
— для получения от объекта промежуточного примитивного значения, которое будет конвертировано в строку.
ToString()
интересным образом отличается от работы функции String()
. Если argument
является символом, то первая выбрасывает TypeError
, а вторая — нет. Так происходит, потому что по умолчанию конвертирование символов в строку приводит к исключению:
const sym = Symbol('sym');
''+sym
// TypeError: Cannot convert a Symbol value to a string
`${sym}`
// TypeError: Cannot convert a Symbol value to a string
String()
и Symbol.prototype.toString()
переопределяют это поведение (обе функции описаны далее в статье):
String(sym)
// 'Symbol(sym)'
sym.toString()
// 'Symbol(sym)'
String()
function String(value) {
let s;
if (value === undefined) {
s = '';
} else {
if (new.target === undefined && TypeOf(value) === 'symbol') { // (A)
return SymbolDescriptiveString(value);
}
s = ToString(value);
}
if (new.target === undefined) {
// Function call
return s;
}
// New call
return StringCreate(s, new.target.prototype); // simplified!
}
Мы видим, что String()
ведет себя по-разному, если вызывается как функция и если вызывается как конструктор (new
).
Вспомогательные функции StringCreate()
и SymbolDescriptiveString()
выглядят следующим образом:
function StringCreate(value, prototype) {
// Create a new String instance that has the given prototype
}
function SymbolDescriptiveString(sym) {
assert.equal(TypeOf(sym), 'symbol');
let desc = sym.description;
if (desc === undefined) {
desc = '';
}
assert.equal(TypeOf(desc), 'string');
return 'Symbol('+desc+')';
}
Symbol.prototype.toString()
В дополнение к String()
, вы можете использовать метод .toString()
для конвертации символов в строку. Его спецификация выглядит следующим образом:
Symbol.prototype.toString = function () {
let sym = thisSymbolValue(this);
return SymbolDescriptiveString(sym);
};
function thisSymbolValue(value) {
if (TypeOf(value) === 'symbol') {
return value;
}
if (TypeOf(value) === 'object' && '__SymbolData__' in value) {
let s = value.__SymbolData__;
assert.equal(TypeOf(s), 'symbol');
return s;
}
}
Object.prototype.toString()
Вот так выглядит дефолтная спецификация метода .toString()
:
Object.prototype.toString = function () {
if (this === undefined) {
return '[object Undefined]';
}
if (this === null) {
return '[object Null]';
}
let O = ToObject(this);
let isArray = Array.isArray(O);
let builtinTag;
if (isArray) {
builtinTag = 'Array';
} else if ('__ParameterMap__' in O) {
builtinTag = 'Arguments';
} else if ('__Call__' in O) {
builtinTag = 'Function';
} else if ('__ErrorData__' in O) {
builtinTag = 'Error';
} else if ('__BooleanData__' in O) {
builtinTag = 'Boolean';
} else if ('__NumberData__' in O) {
builtinTag = 'Number';
} else if ('__StringData__' in O) {
builtinTag = 'String';
} else if ('__DateValue__' in O) {
builtinTag = 'Date';
} else if ('__RegExpMatcher__' in O) {
builtinTag = 'RegExp';
} else {
builtinTag = 'Object';
}
let tag = O[Symbol.toStringTag];
if (TypeOf(tag) !== 'string') {
tag = builtinTag;
}
return '[object ' + tag + ']';
};
Эта операция используется, если вы конвертируете в строку обычные объекты:
String({})
// '[object Object]'
По умолчанию она также применяется для конвертации инстансов классов:
class MyClass {}
assert.equal(
String(new MyClass()), '[object Object]');
Вы можете изменять, что следует за словом object
в квадратных скобках:
class MyClass {}
MyClass.prototype[Symbol.toStringTag] = 'Custom!';
assert.equal(
String(new MyClass()), '[object Custom!]');
Если вы напрямую вызываете Object.prototype.toString
, вы можете получить доступ к переопределенному поведению:
Object.prototype.toString.call(['a', 'b'])
// '[object Array]'
String(['a', 'b'])
//'a,b'
ToPropertyKey()
Операция ToPropertyKey()
среди прочего используется оператором квадратных скобок ([]
):
function ToPropertyKey(argument) {
let key = ToPrimitive(argument, 'string'); // (A)
if (TypeOf(key) === 'symbol') {
return key;
}
return ToString(key);
}
Опять же, здесь предварительно объекты преобразуются в примитивные значения.
ToNumeric() и связанные операции
Операция ToNumeric() среди прочего используется оператором умножения (*
).
function ToNumeric(value) {
let primValue = ToPrimitive(value, 'number');
if (TypeOf(primValue) === 'bigint') {
return primValue;
}
return ToNumber(primValue);
}
ToNumber()
Операция ToNumber()
выглядит следующим образом:
function ToNumber(argument) {
if (argument === undefined) {
return NaN;
} else if (argument === null) {
return +0;
} else if (argument === true) {
return 1;
} else if (argument === false) {
return +0;
} else if (TypeOf(argument) === 'number') {
return argument;
} else if (TypeOf(argument) === 'string') {
return parseTheString(argument); // not shown here
} else if (TypeOf(argument) === 'symbol') {
throw new TypeError();
} else if (TypeOf(argument) === 'bigint') {
throw new TypeError();
} else {
// argument is an object
let primValue = ToPrimitive(argument, 'number');
return ToNumber(primValue);
}
}
Структура ToNumber()
очень напоминает структуру ToString()
.
Операции с преобразованием типов
Оператор сложения (+)
Вот так описан в спецификации JavaScript оператор сложения:
function Addition(leftHandSide, rightHandSide) {
let lprim = ToPrimitive(leftHandSide);
let rprim = ToPrimitive(rightHandSide);
if (TypeOf(lprim) === 'string' || TypeOf(rprim) === 'string') { // (A)
return ToString(lprim) + ToString(rprim);
}
let lnum = ToNumeric(lprim);
let rnum = ToNumeric(rprim);
if (TypeOf(lnum) !== TypeOf(rnum)) {
throw new TypeError();
}
let T = Type(lnum);
return T.add(lnum, rnum); // (B)
}
Шаги этого алгоритма:
- Оба операнда конвертируются в примитивные значения.
- Если один из операндов является строкой, то оба преобразуются в строки, и возвращается результат их конкатенации.
- Иначе оба оператора преобразуются в числа, и возвращается результат их сложения.
Нестрогое равенство (==)

function abstractEqualityComparison(x, y) {
if (TypeOf(x) === TypeOf(y)) {
// Use strict equality (===)
return strictEqualityComparison(x, y);
}
// Comparing null with undefined
if (x === null && y === undefined) {
return true;
}
if (x === undefined && y === null) {
return true;
}
// Comparing a number and a string
if (TypeOf(x) === 'number' && TypeOf(y) === 'string') {
return abstractEqualityComparison(x, Number(y));
}
if (TypeOf(x) === 'string' && TypeOf(y) === 'number') {
return abstractEqualityComparison(Number(x), y);
}
// Comparing a bigint and a string
if (TypeOf(x) === 'bigint' && TypeOf(y) === 'string') {
let n = StringToBigInt(y);
if (Number.isNaN(n)) {
return false;
}
return abstractEqualityComparison(x, n);
}
if (TypeOf(x) === 'string' && TypeOf(y) === 'bigint') {
return abstractEqualityComparison(y, x);
}
// Comparing a boolean with a non-boolean
if (TypeOf(x) === 'boolean') {
return abstractEqualityComparison(Number(x), y);
}
if (TypeOf(y) === 'boolean') {
return abstractEqualityComparison(x, Number(y));
}
// Comparing an object with a primitive
// (other than undefined, null, a boolean)
if (['string', 'number', 'bigint', 'symbol'].includes(TypeOf(x))
&& TypeOf(y) === 'object') {
return abstractEqualityComparison(x, ToPrimitive(y));
}
if (TypeOf(x) === 'object'
&& ['string', 'number', 'bigint', 'symbol'].includes(TypeOf(y)) {
return abstractEqualityComparison(ToPrimitive(x), y);
}
// Comparing a bigint with a number
if ((TypeOf(x) === 'bigint' && TypeOf(y) === 'number')
|| (TypeOf(x) === 'number' && TypeOf(y) === 'bigint')) {
if ([NaN, +Infinity, -Infinity].includes(x)
|| [NaN, +Infinity, -Infinity].includes(y)) {
return false;
}
if (isSameMathematicalValue(x, y)) {
return true;
} else {
return false;
}
}
return false;
}
Следующие вспомогательные операции в статье не описаны:
strictEqualityComparison()
StringToBigInt()
isSameMathematicalValue()
Глоссарий
Разобравшись немного с приведением типов в JavaScript, давайте составим небольшое резюме в виде словарика терминов:
- При преобразовании типов (type conversion) мы хотим, чтобы выходное значение имело некоторый заданный тип. Если входное значение уже имеет этот тип, то оно просто возвращается без изменений. В противном случае, оно будет сконвертировано по определенным правилам к желаемому типу.
- Явное преобразование типов (explicit type conversion) означает, что программист использует операцию (функцию или оператор) для запуска преобразования типов. Явное преобразование может быть:
- Проверенным (Checked): Если преобразование невозможно, выбрасывается исключение.
- Непроверенным (Unchecked): Если преобразование невозможно, возвращается ошибка.
- Приведение типов (type coercion) — это неявное преобразование типов. Операции автоматически преобразуют свои аргументы в те типы, которые им нужны. Такое преобразование может быть и проверенным, и непроверенным.
0 комментариев