Автор материала, перевод которого мы сегодня публикуем, предлагает взглянуть на то, как выглядит явное и неявное приведение типов на низком уровне. Это позволит всем желающим лучше понять процессы, скрытые в недрах 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?
Комментарии (19)
Zenitchik
16.04.2018 14:04А что из этого нельзя было прочитать в спецификации языка?
virtyaluk
16.04.2018 14:21+1Никогда не понимал людей, который плодят подобную писанину, в то время, как спецификация исчерпывающе все объясняет. Мало того, все то-же самое очень детально описано в трудах авторов типа Николаса Закаса и Кайла Симсона.
yokotoka
16.04.2018 22:56-1А я не понимаю тех, кто тыкает в спецификацию. Вы когда машину покупаете — читаете всю 1500-страничную спецификацию? Или садитесь и едете? Вы уверены, что авторы всех пакетов в npm по всей цепочке зависимостей, которые вы используете, читали спецификации? Вы читаете код каждого пакета и зависимостей, которые используете? Язык, который не бьёт по рукам, когда программист пытается сделать говно в итоге приводит к тому, что дятел влетает в форточку и все взрывается. Можно же сделать интерпретатор со строгой динамической типизацией, который запретит неявное приведение типов. И массив со строкой не даст сложить и покажет, насколько глубоко все испорчено уже.
Gennadii_M
17.04.2018 08:47Я читаю всю книжку после покупки машины, просматриваю большинство npm модулей, которые ставлю себе в зависимости, но не могу не согласиться, что спеки читает далеко не каждый. Совсем далеко. И прочитать такую статью о самом главном по топику легче, чем закопаться в спеку. Поэтому статья очень даже хороша и к месту.
Suvitruf
16.04.2018 16:10-1Если для понимаю таких вещей нужно лезть в спецификацию каждый раз, то язык спроектирован не очень хорошо.
Gennadii_M
17.04.2018 08:50Почему же каждый раз? Достаточно 1 раз понять, как это работает и жить счастливо. Что в статье есть из правил?
1. Бинарный оператор "+" с не строками пытается выполнить операцию сложения
2. Если хотя бы 1 операнд строка — конкатенцаия
3. При неявном приведении разных типов всё сводится к строкам.
Вот как бы и всё кажись? Там реально не всё так сложно, если понять основы.
masai
16.04.2018 14:05Как вы относитесь к неявному приведению типов в JavaScript?
Всё это, конечно, прикольно и, наверное, кому-то удобно. Но лучше бы в языке было поменьше вещей, способных удивить в самый неожиданный момент.
Zenitchik
16.04.2018 14:19Как ни странно — согласен. Неявное приведение типов в большинстве случаев работает логично, но есть
"" == 0
и оно портит картину в случаях, когда a.toString() == ""
Если бы было isNaN("") — сюрпризов было бы в разы меньше.Aquahawk
16.04.2018 14:26поэтому вместо isNaN(x) я пользуюсь конструкцией x !== x. Точно также проверяет на nan, но не приводит типы.
TheShock
16.04.2018 14:30Если бы было isNaN("") — сюрпризов было бы в разы меньше.
В чем логика? Ведь строка — это не NaN. Вы топите за убирание неявного приведения, а потом просите неявное приведение?Zenitchik
16.04.2018 15:08Вы топите за убирание неявного приведения
Простите, в каком месте?
Я ничего не имею против неявного приведения типов, мне не нравится, что пустая строка при приведении к числовому типу даёт 0, а не NaN.
siziyman
16.04.2018 16:20Пустая строка — это число?
TheShock
16.04.2018 16:21Это не число, но это и не NaN. Поймите, что NaN — это особое значение, результат математической операции, а не любое не-число.
siziyman
16.04.2018 16:26NaN — «Not a Number». И NaN не является результатом корректной математической операции.
TheShock
16.04.2018 16:38Как вам выше сказал Zenitchik — это не любое «не число». Если его заодно подтянуть под кое-какие нюансы ЖС — вы еще больше запутаете ситуацию.
siziyman
16.04.2018 16:48+1Про то, что это спецзначение стандартизированного числового типа данных, принятое для обозначения не-числа, я знаю.
Про то, что множество NaN'ов не обозначает (в совокупности, т.е. хотя бы один NaN не обозначает) любое конкретное «не число» — это спорное замечание.
Доступа к тексту стандарта у меня нет (есть подозрение, что он вообще платный), но на многих ресурсах, включая, например, документацию MDN , я вижу следующее (или аналогичное):
It is the returned value when Math functions fail (Math.sqrt(-1)) or when a function trying to parse a number fails (parseInt(«blabla»)).
Ещё прекраснее описанное там же отличие между поведением isNaN() и Number.isNaN().
Хоть убейте, это нельзя назвать логичным и последовательным дизайном языка.
apapacy
16.04.2018 16:16+1Функция parseInt, после разбора переданного ей аргумента, округляет полученные числа.
…
Эта функция неправильно работает с очень большими числами, поэтому её не следует рассматривать в качестве альтернативы функции Math.floor (она, кстати, тоже выполняет приведение типов).
Оба утвержедния не соответсвует истине. Фнукция отбрасывает все что не соответсвует формату целых чисел то есть например начиная с десятичной точки. То есть фактически отрасывает дробную часть.
Аналогичено и с большими числами. 1е2 можно спорить о том насколько большое это число. но точно 1 получится потому что там с «е» начинается нарушение формата целого числа
alex6636
Весь язык как та картинка