https://habrahabr.ru/company/ruvds/blog/347866/- Разработка веб-сайтов
- JavaScript
- Блог компании RUVDS.com
Приведение типов — это процесс преобразования значений из одного типа в другой (например — строки в число, объекта — в логическое значение, и так далее). Любой тип в JavaScript, идёт ли речь о примитивном типе, или об объекте, может быть преобразован в другой тип. Напомним, что примитивными типами данных в JS являются
Number
,
String
,
Boolean
,
Null
,
Undefined
. К этому списку в ES6 добавился тип
Symbol
, который ведёт себя совсем не так, как другие типы. Явное приведение типов — процесс простой и понятный, но всё меняется, когда дело доходит до неявного приведения типов. Тут то, что происходит в JavaScript, некоторые считают странным или нелогичным, хотя, конечно, если заглянуть в стандарты, становится понятно, что все эти «странности» являются особенностями языка. Как бы там ни было, любому JS-разработчику периодически приходится сталкиваться с неявным приведением типов, к тому же, каверзные вопросы о приведении типов вполне могут встретиться на собеседовании.

Эта статья посвящена особенностям работы механизмов приведения типов в JavaScript. Начнём мы её со списка выражений, результаты вычисления которых могут выглядеть совершенно неожиданными. Вы можете испытать себя, попытавшись найти значения этих выражений, не подглядывая в конец статьи, где будет приведён их разбор.
Проверь себя
Вот список интересных выражений, о которых мы только что говорили:
true + false
12 / "6"
"number" + 15 + 3
15 + 3 + "number"
[1] > null
"foo" + + "bar"
'true' == true
false == 'false'
null == ''
!!"false" == !!"true"
[‘x’] == ‘x’
[] + null + 1
0 || "0" && {}
[1,2,3] == [1,2,3]
{}+[]+{}+[1]
!+[]+[]+![]
new Date(0) - 0
new Date(0) + 0
Тут полно такого, что выглядит более чем странно, но без проблем работает в JS, задействуя неявное приведение типов. В подавляющем большинстве случаев неявного приведения типов в JS лучше всего избегать. Рассматривайте этот список как упражнение для проверки ваших знаний о том, как работает приведение типов в JavaScript. Если же тут для вас ничего нового не нашлось — загляните на
wtfjs.com.
JavaScript полон странностей
Вот страница с
таблицей, в которой показаны особенности поведения оператора нестрогого равенства в JavaScript,
==
, при сравнении значений разных типов. Неявное преобразование типов, выполняемое оператором
==
, делает эту таблицу гораздо менее понятной и логичной, чем, скажем, таблица для оператора строгого равенства,
===
, ссылку на которую можно найти на вышеупомянутой странице. Заучить таблицу сравнений для оператора
==
практически невозможно. Но запоминать всё это и не нужно — достаточно освоить принципы преобразования типов, применяемые в JavaScript.
Неявное преобразование типов и явное преобразование типов
Преобразование типов может быть явным и неявным. Когда разработчик выражает намерение сконвертировать значение одного типа в значение другого типа, записывая это соответствующим образом в коде, скажем, в виде
Number(value)
, это называется явным приведением типов (или явным преобразованием типов).
Так как JavaScript — это язык со слабой типизацией, значения могут быть конвертированы между различными типами автоматически. Это называют неявным приведением типов. Обычно такое происходит, когда в выражениях используют значения различных типов, вроде
1 == null
,
2/’5'
,
null + new Date()
. Неявное преобразование типов может быть вызвано и контекстом выражения, вроде
if (value) {…}
, где
value
неявно приводится к логическому типу данных.
Существует оператор, который не вызывает неявного преобразование типов — это оператор строгого равенства,
===
. Оператор нестрогого равенства,
==
, с другой стороны, выполняет и операцию сравнения, и, если нужно, выполняет неявное преобразование типов.
Неявное преобразование типов — палка о двух концах: это источник путаницы и ошибок, но это и полезный механизм, который позволяет писать меньше кода без потери его читабельности.
Три вида преобразования типов
Первая особенность работы с типами в JS, о которой нужно знать, заключается в том, что здесь есть только три вида преобразований:
- В строку (
String
)
- В логическое значение (
Boolean
)
- В число (
Number
)
Вторая особенность JS, которую нужно учитывать, заключается в том, логика преобразования для примитивных типов и для объектов работает по-разному, но и примитивы и объекты могут быть конвертированы в эти три типа. Начнём с примитивных типов данных.
Примитивные типы данных
▍Преобразование к типу String
Для того чтобы явно преобразовать значение в строку, можно воспользоваться функцией
String()
. Неявное преобразование вызывает использование обычного оператора сложения,
+
, с двумя операндами, если один из них является строкой:
String(123) // явное преобразование
123 + '' // неявное преобразование
Все примитивные типы преобразуются в строки вполне естественным и ожидаемым образом:
String(123) // '123'
String(-12.3) // '-12.3'
String(null) // 'null'
String(undefined) // 'undefined'
String(true) // 'true'
String(false) // 'false'
В случае с типом
Symbol
дело несколько усложняется, так как значения этого типа можно преобразовать к строковому типу только явно.
Здесь можно почитать подробности о правилах преобразования типа Symbol.
String(Symbol('my symbol')) // 'Symbol(my symbol)'
'' + Symbol('my symbol') // ошибка TypeError
▍Преобразование к типу Boolean
Для того, чтобы явно преобразовать значение к логическому типу, используют функцию
Boolean()
. Неявное преобразование происходит в логическом контексте, или вызывается логическими операторами (
||
&&
!
).
Boolean(2) // явное преобразование
if (2) { ... } // неявное преобразование в логическом контексте
!!2 // неявное преобразование логическим оператором
2 || 'hello' // неявное преобразование логическим оператором
Обратите внимание на то, что операторы, вроде
||
и
&&
выполняют преобразование значений к логическому типу для внутренних целей, а
возвращают значения исходных операндов, даже если они не являются логическими.
// это выражение возвращает число 123, а не true
// 'hello' и 123 неявно преобразуются к логическому типу при работе оператора && для вычисления значения выражения
let x = 'hello' && 123; // x === 123
Так как при приведении значения к логическому типу возможны лишь два результата —
true
или
false
, легче всего освоить этот вид преобразований, запомнив те выражения, которые выдают
false
:
Boolean('') // false
Boolean(0) // false
Boolean(-0) // false
Boolean(NaN) // false
Boolean(null) // false
Boolean(undefined) // false
Boolean(false) // false
Любое значение, не входящее в этот список, преобразуется в
true
, включая объекты, функции, массивы, даты, а также типы, определённые пользователем. Значения типа
Symbol
также преобразуются в
true
. Пустые объекты и пустые массивы тоже преобразуются в
true
:
Boolean({}) // true
Boolean([]) // true
Boolean(Symbol()) // true
!!Symbol() // true
Boolean(function() {}) // true
▍Преобразование к типу Number
Явное преобразование к числовому типу выполняется с помощью функции
Number()
— то есть по тому же принципу, который используется для типов
Boolean
и
String
.
Неявное приведение значения к числовому типу — тема более сложная, так как оно применяется, пожалуй, чаще чем преобразование в строку или в логическое значение. А именно, преобразование к типу
Number
выполняют следующие операторы:
- Операторы сравнения (
>
, <
, <=
, >=
).
- Побитовые операторы (
|
, &
, ^
, ~
).
- Арифметические операторы (
-
, +
, *
, /
, %
). Обратите внимание на то, что оператор +
с двумя операндами не вызывает неявное преобразование к числовому типу, если хотя бы один оператор является строкой.
- Унарный оператор
+
.
- Оператор нестрогого равенства
==
(а также !=
). Обратите внимание на то, что оператор ==
не производит неявного преобразования в число, если оба операнда являются строками.
Number('123') // явное преобразование
+'123' // неявное преобразование
123 != '456' // неявное преобразование
4 > '5' // неявное преобразование
5/null // неявное преобразование
true | 0 // неявное преобразование
Вот как в числа преобразуются примитивные значения:
Number(null) // 0
Number(undefined) // NaN
Number(true) // 1
Number(false) // 0
Number(" 12 ") // 12
Number("-12.34") // -12.34
Number("\n") // 0
Number(" 12s ") // NaN
Number(123) // 123
При преобразовании строк в числа система сначала обрезает пробелы, а также символы
\n
и
\t
, находящиеся в начале или в конце строки, и возвращает
NaN
, если полученная строка не является действительным числом. Если строка пуста — возвращается
0
.
Значения
null
и
undefined
обрабатываются иначе:
null
преобразуется в
0
, в то время как
undefined
превращается в
NaN
.
Значения типа
Symbol
не могут быть преобразованы в число ни явно, ни неявно. Более того, при попытке такого преобразования выдаётся ошибка
TypeError
. Можно было бы ожидать, что подобное вызовет преобразование значения типа
Symbol
в
NaN
, как это происходит с
undefined
, но этого не происходит. Подробности о правилах преобразования значений типа
Symbol
вы можете найти на
MDN.
Number(Symbol('my symbol')) // Ошибка TypeError
+Symbol('123') // Ошибка TypeError
Вот два особых правила, которые стоит запомнить:
При применении оператора
==
к
null
или
undefined
преобразования в число не производится. Значение
null
равно только
null
или
undefined
и не равно ничему больше.
null == 0 // false, null не преобразуется в 0
null == null // true
undefined == undefined // true
null == undefined // true
Значение
NaN
не равно ничему, включая себя. В следующем примере, если значение не равно самому себе, значит мы имеем дело с
NaN
if (value !== value) { console.log("we're dealing with NaN here") }
Преобразование типов для объектов
Итак, мы рассмотрели преобразование типов для примитивных значений. Тут всё довольно просто. Когда же дело доходит до объектов, и система встречает выражения вроде
[1] + [2,3]
, сначала ей нужно преобразовать объект в примитивное значение, которое затем преобразуется в итоговой тип. При работе с объектами, напомним, также существует всего три направления преобразований: в число, в строку, и в логическое значение.
Самое простое — это преобразование в логическое значение: любое значение, не являющееся примитивом, всегда неявно конвертируется в
true
, это справедливо и для пустых объектов и массивов.
Объекты преобразуются в примитивные значения с использованием внутреннего метода
[[ToPrimitive]]
, который ответственен и за преобразование в числовой тип, и за преобразование в строку.
Вот псевдо-реализация метода
[[ToPrimitive]]
:
function ToPrimitive(input, preferredType){
switch (preferredType){
case Number:
return toNumber(input);
break;
case String:
return toString(input);
break
default:
return toNumber(input);
}
function isPrimitive(value){
return value !== Object(value);
}
function toString(){
if (isPrimitive(input.toString())) return input.toString();
if (isPrimitive(input.valueOf())) return input.valueOf();
throw new TypeError();
}
function toNumber(){
if (isPrimitive(input.valueOf())) return input.valueOf();
if (isPrimitive(input.toString())) return input.toString();
throw new TypeError();
}
}
Методу
[[ToPrimitive]]
передаётся входное значение и предпочитаемый тип, к которому его надо преобразовать:
Number
или
String
. При этом аргумент
preferredType
необязателен.
И при конверсии в число, и при конверсии в строку используются два метода объекта, передаваемого
[[ToPrimitive]]
: это
valueOf
и
toString
. Оба метода объявлены в
Object.prototype
, и, таким образом, доступны для любого типа, основанного на
Object
, например — это
Date
,
Array
, и так далее.
В целом, работа алгоритма выглядит следующим образом:
- Если входное значение является примитивом — не делать ничего и вернуть его.
- Вызвать
input.toString()
, если результат является значением примитивного типа — вернуть его.
- Вызвать
input.valueOf()
, если результат является значением примитивного типа — вернуть его.
- Если ни
input.toString()
, ни input.valueOf()
не дают примитивное значение — выдать ошибку TypeError
.
При преобразовании в число сначала вызывается
valueOf
(3), если результат получить не удаётся — вызывается
toString
(2). При преобразовании в строку используется обратная последовательность действий — сначала вызывается
toString
(2), а в случае неудачи вызывается
valueOf
(3).
Большинство встроенных типов не имеют метода
valueOf
, или имеют
valueOf
, который возвращает сам объект, для которого он вызван (
this
), поэтому такое значение игнорируется, так как примитивом оно не является. Именно поэтому преобразование в число и в строку может работать одинаково — и то и другое сводится к вызову
toString()
.
Различные операторы могут вызывать либо преобразование в число, либо преобразование в строку с помощью параметра
preferredType
. Но есть два исключения: оператор нестрогого равенства
==
и оператор
+
с двумя операндами вызывают конверсию по умолчанию (
preferredType
не указывается или устанавливается в значение
default
). В этом случае большинство встроенных типов рассматривают, как стандартный вариант поведения, конверсию в число, за исключением типа
Date
, который выполняет преобразование объекта в строку.
Вот пример поведения
Date
при преобразовании типов:
let d = new Date();
// получение строкового представления
let str = d.toString(); // 'Wed Jan 17 2018 16:15:42'
// получение числового представления, то есть - числа миллисекунд с начала эпохи Unix
let num = d.valueOf(); // 1516198542525
// сравнение со строковым представлением
// получаем true так как d конвертируется в ту же строку
console.log(d == str); // true
// сравнение с числовым представлением
// получаем false, так как d не преобразуется в число с помощью valueOf()
console.log(d == num); // false
// Результат 'Wed Jan 17 2018 16:15:42Wed Jan 17 2018 16:15:42'
// '+', так же, как и '==', вызывает режим преобразования по умолчанию
console.log(d + d);
// Результат 0, так как оператор '-' явно вызывает преобразование в число, а не преобразование по умолчанию
console.log(d - d);
Стандартные методы
toString()
и
valueOf()
можно переопределить для того, чтобы вмешаться в логику преобразования объекта в примитивные значения.
var obj = {
prop: 101,
toString(){
return 'Prop: ' + this.prop;
},
valueOf() {
return this.prop;
}
};
console.log(String(obj)); // 'Prop: 101'
console.log(obj + '') // '101'
console.log(+obj); // 101
console.log(obj > 100); // true
Обратите внимание на то, что
obj + ‘’
возвращает
‘101’
в виде строки. Оператор
+
вызывает стандартный режим преобразования. Как уже было сказано,
Object
рассматривает приведение к числу как преобразование по умолчанию, поэтому использует сначала метод
valueOf()
а не
toString()
.
Метод Symbol.toPrimitive ES6
В ES5 допустимо менять логику преобразования объекта в примитивное значение путём переопределения методов
toString
и
valueOf
.
В ES6 можно пойти ещё дальше и полностью заменить внутренний механизм
[[ToPrimitive]]
, реализовав метод объекта
[Symbol.toPrimtive]
.
class Disk {
constructor(capacity){
this.capacity = capacity;
}
[Symbol.toPrimitive](hint){
switch (hint) {
case 'string':
return 'Capacity: ' + this.capacity + ' bytes';
case 'number':
// преобразование в KiB
return this.capacity / 1024;
default:
// считаем преобразование в число стандартным
return this.capacity / 1024;
}
}
}
// 1MiB диск
let disk = new Disk(1024 * 1024);
console.log(String(disk)) // Capacity: 1048576 bytes
console.log(disk + '') // '1024'
console.log(+disk); // 1024
console.log(disk > 1000); // true
Разбор примеров
Вооружённые теорией, вернёмся к выражениям, приведённым в начале материала. Вот каковы результаты вычисления этих выражений:
true + false // 1
12 / "6" // 2
"number" + 15 + 3 // 'number153'
15 + 3 + "number" // '18number'
[1] > null // true
"foo" + + "bar" // 'fooNaN'
'true' == true // false
false == 'false' // false
null == '' // false
!!"false" == !!"true" // true
['x'] == 'x' // true
[] + null + 1 // 'null1'
0 || "0" && {} // {}
[1,2,3] == [1,2,3] // false
{}+[]+{}+[1] // '0[object Object]1'
!+[]+[]+![] // 'truefalse'
new Date(0) - 0 // 0
new Date(0) + 0 // 'Thu Jan 01 1970 02:00:00(EET)0'
Разберём каждый из этих примеров.
▍true + false
Оператор
+
с двумя операндами вызывает преобразование к числу для
true
и
false
:
true + false
==> 1 + 0
==> 1
▍12 / '6'
Арифметический оператор деления,
/
, вызывает преобразование к числу для строки
'6'
:
12 / '6'
==> 12 / 6
==>> 2
▍«number» + 15 + 3
Оператор
+
имеет ассоциативность слева направо, поэтому выражение
"number" + 15
выполняется первым. Так как один из операндов является строкой, оператор
+
вызывает преобразование к строке для числа
15
. На втором шаге вычисления выражения
"number15" + 3
обрабатывается точно так же:
"number" + 15 + 3
==> "number15" + 3
==> "number153"
▍15 + 3 + «number»
Выражение
15 + 3
вычисляется первым. Тут совершенно не нужно преобразование типов, так как оба операнда являются числами. На втором шаге вычисляется значение выражения
18 + 'number'
, и так как один из операндов является строкой — вызывается преобразование в строку.
15 + 3 + "number"
==> 18 + "number"
==> "18number"
▍[1] > null
Оператор сравнения
>
выполняет числовое сравнение
[1]
и
null
:
[1] > null
==> '1' > 0
==> 1 > 0
==> true
▍«foo» + + «bar»
Унарный оператор
+
имеет более высокий приоритет, чем обычный оператор
+
. В результате выражение
+'bar'
вычисляется первым. Унарный
+
вызывает для строки
'bar'
преобразование в число. Так как строка не является допустимым числом, в результате получается
NaN
. На втором шаге вычисляется значение выражения
'foo' + NaN
.
"foo" + + "bar"
==> "foo" + (+"bar")
==> "foo" + NaN
==> "fooNaN"
▍'true' == true и false == 'false'
Оператор
==
вызывает преобразование в число, строка
'true'
преобразуется в
NaN
, логическое значение
true
преобразуется в
1
.
'true' == true
==> NaN == 1
==> false
false == 'false'
==> 0 == NaN
==> false
▍null == ''
Оператор
==
обычно вызывает преобразование в число, но это не так в случае со значением
null
. Значение
null
равно только
null
или
undefined
и ничему больше.
null == ''
==> false
▍!!«false» == !!«true»
Оператор
!!
конвертирует строки
'true'
и
'false'
в логическое
true
, так как они являются непустыми строками. Затем оператор
==
просто проверяет равенство двух логических значений
true
без преобразования типов.
!!"false" == !!"true"
==> true == true
==> true
▍['x'] == 'x'
Оператор
==
вызывает для массивов преобразование к числовому типу. Метод объекта
Array.valueOf()
возвращает сам массив, и это значение игнорируется, так как оно не является примитивом. Метод массива
toString()
преобразует массив
['x']
в строку
'x'
.
['x'] == 'x'
==> 'x' == 'x'
==> true
▍[] + null + 1
Оператор
+
вызывает преобразование в число для пустого массива
[]
. Метод объекта
Array
valueOf()
игнорируется, так как он возвращает сам массив, который примитивом не является. Метод массива
toString()
возвращает пустую строку.
На втором шаге вычисляется значение выражения
'' + null + 1
.
[] + null + 1
==> '' + null + 1
==> 'null' + 1
==> 'null1'
▍0 || «0» && {}
Логические операторы
||
и
&&
в процессе работы приводят значение операндов к логическому типу, но возвращают исходные операнды (которые имеют тип, отличный от логического). Значение
0
ложно, а значение
'0'
истинно, так как является непустой строкой. Пустой объект
{}
так же преобразуется к истинному значению.
0 || "0" && {}
==> (0 || "0") && {}
==> (false || true) && true // внутреннее преобразование
==> "0" && {}
==> true && true // внутреннее преобразование
==> {}
▍[1,2,3] == [1,2,3]
Преобразование типов не требуется, так как оба операнда имеют один и тот же тип. Так как оператор
==
выполняет проверку на равенство ссылок на объекты (а не на то, содержат ли объекты одинаковые значения) и два массива являются двумя разными объектами, в результате будет выдано
false
.
[1,2,3] == [1,2,3]
==> false
▍{}+[]+{}+[1]
Все операнды не являются примитивными значениями, поэтому оператор
+
начинается с самого левого и вызывает его преобразование к числу. Метод
valueOf
для типов
Object
и
Array
возвращают сами эти объекты, поэтому это значение игнорируется. Метод
toString()
используется как запасной вариант. Хитрость тут в том, что первая пара фигурных скобок
{}
не рассматривается как объектный литерал, она воспринимается как блок кода, который игнорируется. Вычисление начинается со следующего выражения,
+[]
, которое преобразуется в пустую строку через метод
toString()
, а затем в 0.
{}+[]+{}+[1]
==> +[]+{}+[1]
==> 0 + {} + [1]
==> 0 + '[object Object]' + [1]
==> '0[object Object]' + [1]
==> '0[object Object]' + '1'
==> '0[object Object]1'
▍!+[]+[]+![]
Этот пример лучше объяснить пошагово в соответствии с порядком выполнения операций.
!+[]+[]+![]
==> (!+[]) + [] + (![])
==> !0 + [] + false
==> true + [] + false
==> true + '' + false
==> 'truefalse'
▍new Date(0) — 0
Оператор
-
вызывает преобразование в число для объекта типа
Date
. Метод
Date.valueOf()
возвращает число миллисекунд с начала эпохи Unix.
new Date(0) - 0
==> 0 - 0
==> 0
▍new Date(0) + 0
Оператор
+
вызывает преобразование по умолчанию. Объекты типа
Data
считают таким преобразованием конверсию в строку, в результате используется метод
toString()
, а не
valueOf()
.
new Date(0) + 0
==> 'Thu Jan 01 1970 02:00:00 GMT+0200 (EET)' + 0
==> 'Thu Jan 01 1970 02:00:00 GMT+0200 (EET)0'
Итоги
Преобразование типов — это один из базовых механизмом JavaScript, знание которого является основой продуктивной работы. Надеемся, сегодняшний материал помог тем, кто не очень хорошо разбирался в неявном преобразовании типов, расставить всё по своим местам, а тем, кто уверенно, с первого раза, никуда не подсматривая, смог решить «вступительную задачу», позволил вспомнить какой-нибудь интересный случай из их практики.
Уважаемые читатели! А в вашей практике случалось так, чтобы путаница с неявным преобразованием типов в JavaScript приводила к таинственным ошибкам?