JavaScript позволяет выполнять преобразование типов. Если это делают намеренно, то перед нами — явное приведение типов (type casting или explicit coercion). В том случае, когда это производится автоматически, при попытке выполнения каких-либо операций над значениями различных типов, это называют неявным приведением типов (coercion или implicit coercion).
Автор материала, перевод которого мы сегодня публикуем, предлагает взглянуть на то, как выглядит явное и неявное приведение типов на низком уровне. Это позволит всем желающим лучше понять процессы, скрытые в недрах JavaScript и поможет дать аргументированный ответ на вопрос о том, почему [1] + [2] — [3] === 9.
Явное приведение типов
▍Объектные обёртки примитивных типов
Практически все примитивные типы в JavaScript (исключение составляют null
и undefined
) имеют объектные обёртки, включающие в себя их значения. Подробнее об этом можно почитать здесь. У разработчика есть доступ к конструкторам таких объектов. Данный факт можно использовать для преобразования значений одного типа в значения другого типа.
String(123); // '123'
Boolean(123); // true
Number('123'); // 123
Number(true); // 1
В показанном здесь примере обёртки переменных примитивных типов существуют недолго: после того, как дело сделано, система от них избавляется.
На это следует обращать внимание, так как вышеприведённое утверждение не относится к случаям, когда в подобной ситуации используется ключевое слово new
.
const bool = new Boolean(false);
bool.propertyName = 'propertyValue';
bool.valueOf(); // false
if (bool) {
console.log(bool.propertyName); // 'propertyValue'
}
Так как в данном случае bool
— это новый объект (а не примитивное значение), он, в выражении if
, преобразуется к true
.
Более того, можно говорить о равнозначности следующих двух конструкций. Вот этой:
if (1) {
console.log(true);
}
И этой:
if ( Boolean(1) ) {
console.log(true);
}
Можете убедиться в этом сами, проведя следующий эксперимент, в котором используется оболочка Bash. Поместим первый фрагмент кода в файл if1.js
, второй — в файл if2.js
. Теперь выполним следующее:
1. Скомпилируем код на JavaScript, преобразовав его в код на ассемблере, воспользовавшись Node.js.
$ node --print-code ./if1.js >> ./if1.asm
$ node --print-code ./if2.js >> ./if2.asm
2. Подготовим скрипт для сравнения четвёртой колонки (тут находятся команды на ассемблере) получившихся файлов. Здесь намеренно не производится сравнение адресов памяти, так как они могут различаться.
#!/bin/bash
file1=$(awk '{ print $4 }' ./if1.asm)
file2=$(awk '{ print $4 }' ./if2.asm)
[ "$file1" == "$file2" ] && echo "The files match"
3. Запустим этот скрипт. Он выведет следующую строку, что подтверждает идентичность файлов.
"The files match"
▍Функция parseFloat
Функция parseFloat
работает практически так же, как и конструктор Number
, но она свободнее относится к передаваемым ей аргументам. Если ей встречается символ, который не может быть частью числа, то она возвращает значение, являющееся числом, собранным из цифр, находящихся до этого символа и игнорирует остаток переданной ей строки.
Number('123a45'); // NaN
parseFloat('123a45'); // 123
▍Функция parseInt
Функция parseInt
, после разбора переданного ей аргумента, округляет полученные числа. Она может работать со значениями, представленными в разных системах счисления.
parseInt('1111', 2); // 15
parseInt('0xF'); // 15
parseFloat('0xF'); // 0
Функция parseInt
может либо «догадаться» о том, какая система счисления применяется для записи переданного ей аргумента, либо воспользуется «подсказкой» в виде второго аргумента. О правилах, применяемых при использовании этой функции, можно почитать на MDN.
Эта функция неправильно работает с очень большими числами, поэтому её не следует рассматривать в качестве альтернативы функции Math.floor (она, кстати, тоже выполняет приведение типов).
parseInt('1.261e7'); // 1
Number('1.261e7'); // 12610000
Math.floor('1.261e7') // 12610000
Math.floor(true) // 1
▍Функция toString
С помощью функции toString
можно конвертировать в строки значения других типов. При этом надо отметить, что реализация этой функции в прототипах объектов разных типов различается. Если вы чувствуете, что вам нужно лучше разобраться с концепцией прототипов в JavaScript, взгляните на этот материал.
Функция String.prototype.toString
Эта функция возвращает значение, представленное в виде строки.
const dogName = 'Fluffy';
dogName.toString() // 'Fluffy'
String.prototype.toString.call('Fluffy') // 'Fluffy'
String.prototype.toString.call({}) // Uncaught TypeError: String.prototype.toString requires that 'this' be a String
Функция Number.prototype.toString
Эта функция возвращает число, преобразованное в строку (в качестве первого аргумента ей можно передать основание системы счисления, в которой должен быть представлен возвращаемый ею результат).
(15).toString(); // "15"
(15).toString(2); // "1111"
(-15).toString(2); // "-1111"
Функция Symbol.prototype.toString
Эта функция возвращает строковое представление объекта типа Symbol. Выглядит это так: `Symbol(${description})`
. Здесь, для того, чтобы продемонстрировать работу данной функции, используется концепция шаблонных строк.
Функция Boolean.prototype.toString
Эта функция возвращает true
или false
.
Функция Object.prototype.toString
У объектов имеется внутренне значение [[Class]]
. Оно является тегом, представляющим тип объекта. Функция Object.prototype.toString
возвращает строку следующего вида: `[object ${tag}]`
. Тут, в качестве тега, используются либо стандартные значения (например — «Array», «String», «Object», «Date»), либо значения, заданные разработчиком.
const dogName = 'Fluffy';
dogName.toString(); // 'Fluffy' (здесь вызывается String.prototype.toString)
Object.prototype.toString.call(dogName); // '[object String]'
С появлением ES6 теги задают с использованием объектов типа Symbol. Приведём пару примеров. Вот первый.
const dog = { name: 'Fluffy' }
console.log( dog.toString() ) // '[object Object]'
dog[Symbol.toStringTag] = 'Dog';
console.log( dog.toString() ) // '[object Dog]'
Вот второй.
const Dog = function(name) {
this.name = name;
}
Dog.prototype[Symbol.toStringTag] = 'Dog';
const dog = new Dog('Fluffy');
dog.toString(); // '[object Dog]'
Тут также можно использовать классы ES6 с геттерами.
class Dog {
constructor(name) {
this.name = name;
}
get [Symbol.toStringTag]() {
return 'Dog';
}
}
const dog = new Dog('Fluffy');
dog.toString(); // '[object Dog]'
Функция Array.prototype.toString
Эта функция, при вызове её у объекта типа Array
, выполняет вызов toString
для каждого элемента массива, собирает полученные результаты в строку, элементы которой разделены запятыми, и возвращает эту строку.
const arr = [
{},
2,
3
]
arr.toString() // "[object Object],2,3"
Неявное приведение типов
Если вы знаете о том, как работает явное приведение типов в JavaScript, вам гораздо легче будет понять особенности работы неявного приведения типов.
▍Математические операторы
Знак «плюс»
Выражения с двумя операндами, между которыми стоит знак +
, и один из которых является строкой, выдают строку.
'2' + 2 // 22
15 + '' // '15'
Если воспользоваться знаком +
в выражении с одним строковым операндом, его можно преобразовать в число:
+'12' // 12
Другие математические операторы
При применении других математических операторов, таких, как -
или /
, операнды всегда преобразуются к числам.
new Date('04-02-2018') - '1' // 1522619999999
'12' / '6' // 2
-'1' // -1
При преобразовании дат в числа получают Unix-время, соответствующее датам.
▍Восклицательный знак
Использование в выражениях восклицательного знака приводит к выводу true
если исходное значение воспринимается как ложное, и false
— для значений, воспринимаемых системой как истинные. В результате, восклицательный знак, применённый дважды, можно использовать для преобразования различных значений к соответствующим им логическим значениям.
!1 // false
!!({}) // true
▍Функция ToInt32 и побитовый оператор OR
Тут стоит сказать о функции ToInt32
, хотя это — абстрактная операция (внутренний механизм, вызвать который в обычном коде нельзя). ToInt32
преобразует значения в 32-битные целые числа со знаком.
0 | true // 1
0 | '123' // 123
0 | '2147483647' // 2147483647
0 | '2147483648' // -2147483648 (слишком большое)
0 | '-2147483648' // -2147483648
0 | '-2147483649' // 2147483647 (слишком маленькое)
0 | Infinity // 0
Применение побитового оператора OR
в том случае, если один из операндов является нулём, а второй — строкой, приведёт к тому, что значение другого операнда не изменится, но будет преобразовано в число.
▍Другие случаи неявного приведения типов
В процессе работы программисты могут сталкиваться и с другими ситуациями, в которых производится неявное приведение типов. Рассмотрим следующий пример.
const foo = {};
const bar = {};
const x = {};
x[foo] = 'foo';
x[bar] = 'bar';
console.log(x[foo]); // "bar"
Это происходит из-за того, что и foo
, и bar
, при приведении их к строке, превращаются в "[object Object]"
. Вот что на самом деле происходит в этом фрагменте кода.
x[bar.toString()] = 'bar';
x["[object Object]"]; // "bar"
Неявное преобразование типов так же происходит с шаблонными строками. Попытаемся в следующем примере переопределить функцию toString
.
const Dog = function(name) {
this.name = name;
}
Dog.prototype.toString = function() {
return this.name;
}
const dog = new Dog('Fluffy');
console.log(`${dog} is a good dog!`); // "Fluffy is a good dog!"
Стоит отметить, что причиной, по которой не рекомендуется пользоваться оператором нестрогого равенства (==
), является тот факт, что этот оператор, при несовпадении типов операндов, производит неявное преобразование типов. Рассмотрим следующий пример.
const foo = new String('foo');
const foo2 = new String('foo');
foo === foo2 // false
foo >= foo2 // true
Так как здесь использовано ключевое слово new
, foo
и foo2
представляют собой обёртки вокруг примитивных значений (а это — строка 'foo'
). Так как соответствующие переменные ссылаются на разные объекты, то в результате сравнения вида foo === foo2
получается false
. Оператор >=
выполняет неявное преобразование типов, вызывая функцию valueOf
для обоих операндов. Из-за этого тут производится сравнение примитивных значений, и в результате вычисления значения выражения foo >= foo2
получается true
.
[1] + [2] – [3] === 9
Полагаем, теперь вам ясно, почему истинно выражение [1] + [2] – [3] === 9
. Однако, всё же, предлагаем его разобрать.
1. В выражении [1] + [2]
производится преобразование операндов к строкам, с применением Array.prototype.toString
, после чего выполняется конкатенация того, что получилось. Как результат, тут мы имеем строку "12"
.
- Надо отметить, что, например, выражение
[1,2] + [3,4]
даст строку"1,23,4"
;
2. При вычислении выражения 12 - [3]
будет выполнено вычитание "3"
из 12
, что даст 9
.
- Тут тоже рассмотрим дополнительный пример. Так, результатом вычисления выражения
12 - [3,4]
будетNaN
, так как система не может неявно привести"3,4"
к числу.
Итоги
Можно встретить множество рекомендаций, авторы которых советуют попросту избегать неявного приведения типов в JavaScript. Однако автор этого материала полагает, что важно разбираться в особенностях работы этого механизма. Вероятно, не стоит стремиться намеренно пользоваться им, но знание о том, как он устроен, несомненно, окажется полезным при отладке кода и поможет избежать ошибок.
Уважаемые читатели! Как вы относитесь к неявному приведению типов в JavaScript?
Автор: ru_vds