Это сообщение изначально было опубликовано на https://kkhanhluu.github.io/types-in-javascript/

Если вы будете искать javascript-мемы в Google, вы получите 296 000 000 результатов, и многие из них касаются крайних случаев на этом языке, как мем выше. Эти крайние случаи странны, непредсказуемы, и их следует избегать, только если мы не знаем, как работает javascript и что происходит под капотом. Когда мы сталкиваемся с такими путаницами, легче сделать из них мем и обвинить язык, чем обвинять себя в непонимании инструмента, который мы используем каждый день. Раньше я был именно таким разработчиком, пока несколько лет назад не увидел на github серию книг Вы не знаете js Кайла Симпсона. Это полностью изменило мое мнение. Потратив годы на изучение сериала и просмотр курсов Кайла, оказалось, что я не так хорошо знаю Javascript, как думал. На моем пути к лучшему пониманию javascript я действительно хочу отметить знания и опыт, которые я получил, и эта серия — начало этого приключения.

Чтобы лучше понять javascript, давайте заглянем в его ядро, которое, по словам Кайла, можно разделить на 3 столпа:

  • Типы
  • Объем
  • Объект и классы

В этом блоге мы рассмотрим первый столп: Типы.

Встроенные типы

Прежде чем углубляться в типы, мы должны прояснить одну вещь: У переменных нет типов, но у значений, которые они содержат. В javascript есть 7 встроенных примитивных типов: null, undefined, boolean, number, string, object, symbol. Оператор typeof может использоваться для их идентификации

console.log(typeof null); // "object"; 😩
console.log(typeof undefined); // "undefined";
console.log(typeof true); // "boolean";
console.log(typeof 25); // "number";
console.log(typeof 'Khanh'); // "string";
console.log(typeof { name: 'Khanh' }); // "object";
console.log(typeof Symbol()); // "symbol";

оператор typeof вернет строку, представляющую тип значения, за исключением, как ни странно, типа null. Этот баг этой фичи стоит с самой первой реализации javascript.

💡 Все эти типы, кроме object, называются "примитивами".

неопределенный против необъявленного

Заманчиво думать, что undefined и undeclared являются синонимами, и эти термины могут использоваться взаимозаменяемо, но на самом деле это два разных понятия. Переменная undefined – это уже объявленная переменная, доступная в области видимости, но в настоящее время не имеющая значения. Напротив, undeclared не объявлен и недоступен в области видимости. Когда мы попытаемся использовать переменную undeclared, будет выброшено ReferenceError

const undefinedVar;
undefinedVar;     // undefined
undeclaredVar;    // ReferenceError: undeclaredVar is not defined

Приведение типа

Приведение, также известное как «преобразование типов», представляет собой механизм преобразования одного типа в другой. Существует два вида принуждения: «неявное» и «явное». Вот пример принуждения, взятый из Вы не знаете js.

var a = 42;
var b = a + ''; // implicit coercion
var c = String(a); // explicit coercion

Как принуждение работает внутри и что происходит под капотом? Чтобы знать внутренние процедуры, нам нужно понимать абстрактные операции.

Абстрактные операции

Каждый раз, когда происходит приведение, оно обрабатывается одной или несколькими абстрактными операциями. Это операции только для внутреннего использования, а не функция, которую можно как-то вызвать. Здесь мы рассмотрим 3 абстрактные операции: ToPrimitive, ToString и ToNumber. Есть и другие операции, на которые можно сослаться и использовать, вы можете проверить спецификацию для получения дополнительной информации.

ToPrimitive

Если у нас есть что-то непримитивное (массив, объект и т. д.) и мы хотим превратить его в примитив, ToPrimitive — это первая абстрактная операция, включающая in. Операция принимает 2 аргумента: ввод и необязательный preferredType (подсказка),, который может быть либо строкой, либо числом. Все встроенные типы, кроме object, являются примитивными, поэтому у каждого непримитивного типа есть 2 доступных метода, производных от Object.prototype: toString() и valueOf(). Если подсказка представляет собой строку, сначала вызывается toString(). Если результатом является примитивное значение, в игру вступает valueOf, и наоборот, если подсказка число.

подсказка: «строка» подсказка: «число» toString()valueOf()valueOf()toString()

ToPrimitive по своей сути является рекурсивным, это означает, что если результат операции не является примитивным, операция будет вызываться снова, пока результат не станет примитивным.

💡 По умолчанию унаследованный метод valueOf от Object.prototype возвращает сам объект. Например, [].valueOf() возвращает [] или {}.valueOf() возвращает {}

Нанизывать

Не путайте ToString и Object.prototype.toString(), это две разные вещи. ToString — это абстрактная операция, внутренняя операция, в то время как Object.prototype.toString() — это функция, производная от Object.prototype и доступная для всех объектов в javascript.

Приведение нестрокового значения к строке осуществляется операцией ToString. Он преобразует значение в соответствии с этой таблицей, и вот несколько примеров:

undefined      ->       'undefined'
null           ->            'null'
true           ->            'true'
15             ->              '15'

Для не примитивных значений будет вызываться ToPrimitive с строкой подсказки, которая, в свою очередь, вызывает Object.prototype.toString(), а затем valueOf() (при необходимости). Реализация по умолчанию Object.prototype.toString() возвращает [Object object]. Сам массив имеет переопределенную реализацию для toString(): он удаляет квадратные скобки и объединяет элемент массива с ,. Это может привести к странным интересным результатам.

[]                           ->    ""   🤔
[1, 2, 3]                    ->    "1, 2, 3"
[null, undefined]            ->    ","  😳
[,,,]                        ->    ",,,"

ToNumber

Операция преобразует нечисловое значение в число согласно этой таблице. Для не примитивных значений будет вызываться ToPrimitive с подсказкой номер, которая, в свою очередь, вызывает valueOf(), а затем Object.prototype.toString() (при необходимости). Потому что valueOf() по умолчанию возвращает сам объект. Давайте возьмем пример, чтобы лучше понять операцию:

[""]    -> 0
  • Поскольку [""] не является примитивным значением, ToPrimitive() будет вызываться с подсказкой номер.
  • Будет вызван valueOf(), который вернет сам объект. Результат valueOf() не является примитивным значением, поэтому в игру вступит Object.prototype.toString().
  • Переопределенная реализация массива toString() удаляет квадратную скобку и объединяет элемент массива с ,, поэтому [""].toString() возвращает "".
  • Посмотрите таблицу, о которой я упоминал выше, пустая строка будет преобразована в 0.

Случаи принуждения

Взяв за основу эти операции абстракции, пришло время заняться темой принуждения. Действительно ли приведение типов — это зло и ужасная часть, которой нам следует избегать? Вы можете утверждать, что избегаете принуждения, потому что оно коррумпировано, но в некоторых случаях принуждение действительно полезно, или вы могли использовать его, не зная об этом.

const age = 29;
console.log(`My brother is ${age} years old`}; // "My brother 25 years old"

Как, черт возьми, javascript может объединить строку «Мой брат» с age, значение которого в настоящее время является числом? Да, вы правы, это принуждение типа. Без приведения типов вам нужно явно преобразовать возраст следующим образом:

const age = 29;
console.log(`My brother is ${String(age)} years old`};
// "My brother 25 years old"
// OR
const age = 29;
console.log(`My brother is ${age.toString()} years old`}; // "My brother 25 years old"

Конечно, я всегда предпочитаю первую версию из-за ее лаконичности и удобочитаемости.

Еще один пример приведения типов, который вы должны были видеть во многих кодовых базах при работе с браузерами:

function addNumber() {
  return +document.getElementById('number').value + 1;
}

Или есть оператор if, использующий приведение типов, который должен написать каждый разработчик js:

if (document.getElementById('number').value) {
  console.log("Oh, that's having a value");
}

Соберите наши знания

Зная некоторые абстрактные операции и то, как они работают, теперь мы должны быть в состоянии объяснить некоторые угловые случаи в приведенном выше меме. Давайте пройдемся по некоторым из них

[] + [] -> ""

Результатом ToString() с пустым массивом является "" , поэтому "" при объединении с "", конечно же, возвращает ""

[] + {} -> "[Object object]"

Это должно быть легко. [] преобразуется в "", а Object.prototype.toString() по умолчанию возвращает "[Object object]", поэтому результат, конечно, строка «[Object object]»

{} + [] -> 0

Хм, это действительно сложно. Поскольку фигурные скобки в начале оператора интерпретируются как начало блока кода, первая пара фигурных скобок интерпретируется как пустой блок кода. Таким образом, это выражение эквивалентно:

+[]                             // The plus here is an unary operator, which converts [] to number
ToNumber([])                    // calls toPrimitive with hint number
ToPrimitive([], 'number')       // calls valueOf() first and then toString() if necessary
                                // [].valueOf() returns [], which is not primitive, so we have to use toString()
Number([].toString())
Number("") -> 0

true + true + true = 3

Плюс здесь — бинарный оператор, поэтому true будет преобразовано в число 1, пожалуйста, обратитесь к таблице, которую я упоминал в ToNumber. Так что да, true + true + true действительно 3 в javascript.

(! + [] + [] + ![]).length = 9

Первый восклицательный знак выполняет логическое приведение, первый унарный оператор плюс обрабатывает числовое приведение. Таким образом, первые три символа !+[] будут сначала выполнять числовое преобразование пустого массива, а затем преобразовывать этот результат в логическое значение. Второй [] будет преобразован в примитив, как я объяснял в предыдущих примерах, а последний [] будет преобразован в логическое значение с помощью [ToBoolean абстрактной операции](https://tc39.es/ecma262/multipage/abstract-operations.html# sec-toboolean), о котором я не упоминаю в этом блоге. Таким образом, это выражение эквивалентно

(!Number([].toString()) + [].toString() + false)
  .length(!Number('') + '' + false)
  .length(!0 + 'false')
  .length(true + 'false').length;
'truefalse'.length = 9;

Краткое содержание

В этом посте мы обратим внимание на системы типов и на то, как работает преобразование типов в javascript. Неявное приведение типов в javascript осуществляется с помощью абстрактных операций. Динамические типы — одна из основных функций JS, но, с другой стороны, она также вызывает споры. Чтобы закончить этот пост, я хотел бы взять цитату Кайла Симпсона из его знаменитой серии Вы не знаете JS

Принуждение имеет плохую репутацию, но на самом деле во многих случаях оно весьма полезно. Важная задача для ответственного разработчика JS — потратить время на изучение всех тонкостей принудительного принуждения, чтобы решить, какие части помогут улучшить код, а каких действительно следует избегать.