Автор материала, перевод которого мы сегодня публикуем, предлагает взглянуть на то, как выглядит явное и неявное приведение типов на низком уровне. Это позволит всем желающим лучше понять процессы, скрытые в недрах 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.asm2. Подготовим скрипт для сравнения четвёртой колонки (тут находятся команды на ассемблере) получившихся файлов. Здесь намеренно не производится сравнение адресов памяти, так как они могут различаться.
#!/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
Весь язык как та картинка