[1] + [2] — [3] === 9!? Исследование внутренних механизмов приведения типов в JavaScript

в 9:41, , рубрики: javascript, Алгоритмы, Блог компании RUVDS.com, разработка, Разработка веб-сайтов

JavaScript позволяет выполнять преобразование типов. Если это делают намеренно, то перед нами — явное приведение типов (type casting или explicit coercion). В том случае, когда это производится автоматически, при попытке выполнения каких-либо операций над значениями различных типов, это называют неявным приведением типов (coercion или implicit coercion).
Автор материала, перевод которого мы сегодня публикуем, предлагает взглянуть на то, как выглядит явное и неявное приведение типов на низком уровне. Это позволит всем желающим лучше понять процессы, скрытые в недрах JavaScript и поможет дать аргументированный ответ на вопрос о том, почему [1] + [2] — [3] === 9.

[1] + [2] — [3]===9!? Исследование внутренних механизмов приведения типов в JavaScript - 1

Явное приведение типов

▍Объектные обёртки примитивных типов

Практически все примитивные типы в 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?

[1] + [2] — [3]===9!? Исследование внутренних механизмов приведения типов в JavaScript - 2

Автор: ru_vds

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js