JavaScript позволяет выполнять преобразование типов. Если это делают намеренно, то перед нами — явное приведение типов (type casting или explicit coercion). В том случае, когда это производится автоматически, при попытке выполнения каких-либо операций над значениями различных типов, это называют неявным приведением типов (coercion или implicit coercion).
Автор материала, перевод которого мы сегодня публикуем, предлагает взглянуть на то, как выглядит явное и неявное приведение типов на низком уровне. Это позволит всем желающим лучше понять процессы, скрытые в недрах JavaScript и поможет дать аргументированный ответ на вопрос о том, почему [1] + [2] — [3] === 9.
Практически все примитивные типы в JavaScript (исключение составляют
В показанном здесь примере обёртки переменных примитивных типов существуют недолго: после того, как дело сделано, система от них избавляется.
На это следует обращать внимание, так как вышеприведённое утверждение не относится к случаям, когда в подобной ситуации используется ключевое слово
Так как в данном случае
Более того, можно говорить о равнозначности следующих двух конструкций. Вот этой:
И этой:
Можете убедиться в этом сами, проведя следующий эксперимент, в котором используется оболочка Bash. Поместим первый фрагмент кода в файл
1. Скомпилируем код на JavaScript, преобразовав его в код на ассемблере, воспользовавшись Node.js.
2. Подготовим скрипт для сравнения четвёртой колонки (тут находятся команды на ассемблере) получившихся файлов. Здесь намеренно не производится сравнение адресов памяти, так как они могут различаться.
3. Запустим этот скрипт. Он выведет следующую строку, что подтверждает идентичность файлов.
Функция
Функция
Функция
Эта функция неправильно работает с очень большими числами, поэтому её не следует рассматривать в качестве альтернативы функции Math.floor (она, кстати, тоже выполняет приведение типов).
С помощью функции
Эта функция возвращает значение, представленное в виде строки.
Эта функция возвращает число, преобразованное в строку (в качестве первого аргумента ей можно передать основание системы счисления, в которой должен быть представлен возвращаемый ею результат).
Эта функция возвращает строковое представление объекта типа Symbol. Выглядит это так:
Эта функция возвращает
У объектов имеется внутренне значение
С появлением ES6 теги задают с использованием объектов типа Symbol. Приведём пару примеров. Вот первый.
Вот второй.
Тут также можно использовать классы ES6 с геттерами.
Эта функция, при вызове её у объекта типа
Если вы знаете о том, как работает явное приведение типов в JavaScript, вам гораздо легче будет понять особенности работы неявного приведения типов.
Выражения с двумя операндами, между которыми стоит знак
Если воспользоваться знаком
При применении других математических операторов, таких, как
При преобразовании дат в числа получают Unix-время, соответствующее датам.
Использование в выражениях восклицательного знака приводит к выводу
Тут стоит сказать о функции
Применение побитового оператора
В процессе работы программисты могут сталкиваться и с другими ситуациями, в которых производится неявное приведение типов. Рассмотрим следующий пример.
Это происходит из-за того, что и
Неявное преобразование типов так же происходит с шаблонными строками. Попытаемся в следующем примере переопределить функцию
Стоит отметить, что причиной, по которой не рекомендуется пользоваться оператором нестрогого равенства (
Так как здесь использовано ключевое слово
Полагаем, теперь вам ясно, почему истинно выражение
1. В выражении
2. При вычислении выражения
Можно встретить множество рекомендаций, авторы которых советуют попросту избегать неявного приведения типов в JavaScript. Однако автор этого материала полагает, что важно разбираться в особенностях работы этого механизма. Вероятно, не стоит стремиться намеренно пользоваться им, но знание о том, как он устроен, несомненно, окажется полезным при отладке кода и поможет избежать ошибок.
Уважаемые читатели! Как вы относитесь к неявному приведению типов в JavaScript?
Автор материала, перевод которого мы сегодня публикуем, предлагает взглянуть на то, как выглядит явное и неявное приведение типов на низком уровне. Это позволит всем желающим лучше понять процессы, скрытые в недрах 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?