Skip to content

Latest commit

 

History

History
1919 lines (1223 loc) · 170 KB

File metadata and controls

1919 lines (1223 loc) · 170 KB

Вы не знаете JS: Типы и грамматика

Глава 4: Приведение

Теперь, когда мы гораздо более полно понимаем типы и значения Javascript, мы обратим наше внимание на очень спорную тему: приведение.

Как мы упоминали в Главе 1, споры о том, является ли приведение полезной возможностью или недостатком языка (или чем-то посередине!) бушевали с первого дня. Если вы читали другие популярные книги по JS, вы знаете, что в подавляющем большинстве случаев распространено мнение о том, что приведение - это волшебство, зло, путаница и просто совершенно плохая идея.

Следуя духу этой серии книг, вместо того, чтобы убегать от приведения значений, потому что так делают все остальные, или потому, что вас укусила какая-то муха, я думаю, вам следует заняться тем, чего вы не понимаете, и попытаться лучше схватить это.

Наша цель - полностью изучить плюсы и минусы (да, есть плюсы!) приведения, чтобы вы могли принять обоснованное решение о его целесообразности в вашем коде.

Конвертация значений

Конвертация значения из одного типа в другой часто называется "преобразованием типа", когда делается явно, и "приведением", когда выполняется неявно (диктуется правилами использования значения).

Примечание: Это может быть неочевидно, но результат приведения в JavaScript это всегда значение скалярного примитива (см. Главу 2), например, string, number, или boolean. Не существует приведения, которое бы приводило к созданию сложного значения, такого как object или function. Глава 3 посвящена "упаковке", которая помещает скалярные примитивные значения в их object-аналоги, но на самом деле это не приведение в строгом смысле слова.

Другой распространенный способ различения этих терминов состоит в следующем: "преобразование типов" (или "конвертация типов") выполняется в статически типизированных языках во время компиляции, а "приведение типов" - это преобразование во время выполнения в динамически типизированных языках.

Однако в JavaScript большинство людей называют все эти виды преобразований приведением, поэтому я предпочитаю обозначать их как "неявное приведение" и "явное приведение".

Разница должна быть очевидной: "явное приведение" - это когда при взгляде на код ясно, что преобразование типа выполняется намеренно, тогда как "неявное приведение" - это когда преобразование - это менее очевидный побочный эффект какой-либо другой намеренной операции.

Для примера рассмотрим эти два способа приведения:

var a = 42;

var b = a + "";			// неявное приведение

var c = String( a );	// явное приведение

В случае с b возникающее приведение происходит неявно, потому что оператор + в сочетании с одним из операндов, являющимся строковым значением (""), будет настаивать на том, что это операция конкатенации строк (сложения двух string вместе), что в качестве (скрытого) побочного эффекта приведет к тому, что значение 42 в a будет приведено к его строковому эквиваленту: "42".

Напротив, функция String(..) делает конвертацию очевидной, так как она явно берёт значение в a и приводит его к string представлению.

Оба подхода приводят к одному и тому же результату: из 42 получается "42". Но именно то, как это делается, лежит в основе жарких дебатов о приведении в JavaScript.

Примечание: Технически, здесь, помимо стилистических различий, есть и нюансы в разном поведении. Мы рассмотрим их более подробно позже в разделе "Неявно: Строки <--> Числа".

Термины "явный" и "неявный", или "очевидный" и "скрытый побочный эффект", относительны.

Если вы точно знаете, что делает a + "", и вы намеренно пишете это, чтобы привести к string, вы можете воспринимать эту операцию достаточно "явной". И наоборот, если вы никогда не видели функцию String(..), используемую для приведения к string, ее поведение может показаться достаточно скрытым, чтобы стать для вас "неявным".

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

Просто помните: часто бывает так, что мы пишем наш код и являемся единственными, кто его читает. Даже если вы являетесь экспертом во всех тонкостях JS, подумайте, что почувствуют ваши менее опытные товарищи по команде, когда прочтут ваш код. Будет ли он "явным" или "неявным" для них так же, как и для вас?

Абстрактные операции со значениями

Прежде чем мы сможем приступить к явному и неявному приведению, нам нужно изучить базовые правила, которые определяют, как значения становятся string, number или boolean. Спецификация ES5 в разделе 9 определяет несколько "абстрактных операций" (затейливо обозначенные как "исключительно внутренние операции") с правилами конвертации значений. Мы специально обратим внимание на: ToString, ToNumber, ToBoolean и, в меньшей степени, на ToPrimitive.

ToString

Когда любое не строковое значение приводится в string-представление, преобразование выполняется абстрактной операцией ToString из раздела 9.8 спецификации.

Встроенные примитивные значения имеют естественную строковое отображение: null становится "null", undefined превращается в "undefined", а true конвертируется в "true". Числа (number) обычно представляются ожидаемым вами образом, но, как мы обсуждали в Главе 2, очень маленькие или очень большие числа представляются в экспоненциальной форме:

// умножим `1.07` на `1000` семь раз
var a = 1.07 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000;

// семь раз по три цифры => 21 цифра
a.toString(); // "1.07e21"

Для обычных объектов, если вы не укажете свой собственный, по умолчанию используется toString() (находится в Object.prototype.toString()), который вернет внутренний [[Class]] (см. Главу 3). Например, "[object Object]".

Но, как было показано ранее, если у объекта есть свой собственный метод toString(), и вы используете этот объект как string, автоматически будет вызван его toString(), и вместо объекта будет использован string-результат этого метода.

Примечание: Технически путь приведения объекта к string пролегает через абстрактную операцию ToPrimitive (Спецификация ES5, раздел 9.1), но эти нюансы более подробно рассматриваются позже в разделе ToNumber, поэтому мы пропустим их здесь.

У массивов базовый метод toString() переопределён и представляет собой сцепление всех его значений в виде строк через разделитель "," (строковое представление каждого значения формируется индивидуально):

var a = [1,2,3];

a.toString(); // "1,2,3"

Опять же, toString() может быть либо вызван явно, либо он будет вызван автоматически, если не строковое значение используется в string-контексте.

Строковое преобразование JSON

Другая задача, которая, казалось бы, плохо связанна с ToString, - это когда вы используете утилиту JSON.stringify(..) для сериализации значения в строковый вид, совместимый с JSON.

Важно отметить, что это строковое преобразование - не совсем то же самое, что приведение. Но поскольку оно связано с приведенными выше правилами ToString, мы сделаем небольшое отступление, чтобы рассмотреть логику строкового преобразования JSON.

Обычно для большинства простых значений строковое преобразование JSON ведет себя так же, как toString(), за исключением того, что результатом сериализации всегда является string:

JSON.stringify( 42 );	// "42"
JSON.stringify( "42" );	// ""42"" (строка с закавыченным строковым значением)
JSON.stringify( null );	// "null"
JSON.stringify( true );	// "true"

Любое безопасное для JSON значение может быть преобразовано в строку с помощью JSON.stringify(..). Но что такое безопасное для JSON? Любое значение, которое может быть корректно отображено в представлении JSON.

Вероятно, проще будет назвать значения, которые не являются безопасными для JSON. Некоторые примеры: undefined, function, symbol (ES6+) и object с циклическими ссылками (где ссылки на свойства объекта создают замкнутый круг, указывая друг на друга). Все это недопустимые значения для стандартной структуры JSON, главным образом потому, что они не переносимы на другие языки, которые используют JSON-данные.

Утилита JSON.stringify(..) автоматически опустит значения undefined, function и symbol, когда столкнется с ними. Если такое значение окажется в array, то оно заменяется на null (чтобы сохранить информацию о местоположении элементов в массиве). Если любое из них хранится в свойстве object, это свойство будет просто исключено.

Рассмотрим:

JSON.stringify( undefined );					// undefined
JSON.stringify( function(){} );					// undefined

JSON.stringify( [1,undefined,function(){},4] );	// "[1,null,null,4]"
JSON.stringify( { a:2, b:function(){} } );		// "{"a":2}"

Но если вы попытаетесь передать в JSON.stringify(..) любой object с циклическими ссылками, то будет выдана ошибка.

Строковое преобразование JSON ведет себя иначе, если в передаваемом ему object определен метод toJSON(). Этот метод будет вызван первым, чтобы получить сериализованное значение.

Если вы собираетесь преобразовывать в JSON строку объект, который может содержать недопустимые JSON-значения, или если у вас в object есть значения, которые не подходят для сериализации, то вам следует определить для него метод toJSON(), который возвратит безопасную для JSON версию object.

Например:

var o = { };

var a = {
	b: 42,
	c: o,
	d: function(){}
};

// создадим циклическую ссылку в `a`
o.e = a;

// выдаст ошибку о циклической ссылке
// JSON.stringify( a );

// определим пользовательскую сериализацию в JSON
a.toJSON = function() {
	// при сериализации включать только свойство `b`
	return { b: this.b };
};

JSON.stringify( a ); // "{"b":42}"

Это очень распространенное заблуждение, что toJSON() должен возвращать строковое представление JSON. Вероятно, это неправильно до тех пор, пока вы не захотите сделать сериализацию самой string (обычно такого желания нет!). В действительности toJSON() должен возвращать обычное значение (любого подходящего типа), а JSON.stringify(..) сам преобразует его в строку.

Другими словами, toJSON() следует интерпретировать как "к JSON-безопасному значению, подходящему для превращения в строку", а не "к строке JSON", как ошибочно предполагают многие разработчики.

Рассмотрим:

var a = {
	val: [1,2,3],

	// вероятно, правильно!
	toJSON: function(){
		return this.val.slice( 1 );
	}
};

var b = {
	val: [1,2,3],

	// вероятно, неправильно!
	toJSON: function(){
		return "[" +
			this.val.slice( 1 ).join() +
		"]";
	}
};

JSON.stringify( a ); // "[2,3]"

JSON.stringify( b ); // ""[2,3]""

Во втором вызове мы сериализовали возвращаемый string, а не array, что, вероятно, было не тем, что мы хотели сделать.

Пока мы говорим о JSON.stringify(..), давайте обсудим некоторые менее известные возможности, которые тем не менее могут быть очень полезными.

В JSON.stringify(..) может быть передан необязательный второй аргумент, который называется replacer. Он может быть либо array, либо function. Аргумент используется для настройки рекурсивной сериализации object, предоставляя механизм фильтрации, какие свойства должны включаться или исключаться, аналогично тому, как toJSON() может готовить значение для сериализации.

Если replacer - array, то он должен быть массивом строк, каждая из которых будет содержать имя свойства, которое разрешено включать в сериализацию объекта. Если в object есть свойство, которого нет в этом списке, оно будет пропущено.

Если replacer - function, то она будет вызвана один раз для самого object, а затем по разу для каждого свойства в object. Каждый раз передается два аргумента, key (ключ) и value (значение). Чтобы пропустить key при сериализации, верните значение undefined. В противном случае верните указанное value.

var a = {
	b: 42,
	c: "42",
	d: [1,2,3]
};

JSON.stringify( a, ["b","c"] ); // "{"b":42,"c":"42"}"

JSON.stringify( a, function(k,v){
	if (k !== "c") return v;
} );
// "{"b":42,"d":[1,2,3]}"

Примечание: Когда replacer - это function, при первом вызове (где передается сам объект a) аргумент k равен undefined. Оператор if отфильтровывает свойство с именем "c". Строковое преобразование рекурсивное, поэтому в массиве [1,2,3] каждое из его значений (1, 2, 3) передается как аргумент v, а индексы (0, 1, 2) - как аргумент k.

В JSON.stringify(..) также может быть передан третий необязательный аргумент, называемый space, который используется в качестве отступа для более наглядного и читаемого вывода. space может быть положительным целым числом, говорящим, сколько пробелов следует использовать на каждом уровне отступа. space может быть и string. В этом случае для каждого уровня отступа будут использоваться первые десять символов его значения.

var a = {
	b: 42,
	c: "42",
	d: [1,2,3]
};

JSON.stringify( a, null, 3 );
// "{
//    "b": 42,
//    "c": "42",
//    "d": [
//       1,
//       2,
//       3
//    ]
// }"

JSON.stringify( a, null, "-----" );
// "{
// -----"b": 42,
// -----"c": "42",
// -----"d": [
// ----------1,
// ----------2,
// ----------3
// -----]
// }"

Помните, что JSON.stringify(..) напрямую не является формой приведения. Однако мы рассмотрели его здесь по двум причинам, которые связывают его действие с приведением ToString:

  1. Значения string, number, boolean и null - все они в основном преобразуются JSON-строки так же, как приводятся в string-значения с помощью правил абстрактной операции ToString.

  2. Если вы передаете object-значение в JSON.stringify(..), и в этом object есть метод toJSON(), то toJSON() автоматически вызывается, чтобы (как бы) "привести" значение в безопасное для JSON значение перед началом преобразования в строку.

ToNumber

Если любое не number значение используется в контексте, требующем, чтобы оно было number, например, в математической операции, то спецификация ES5 определяет абстрактную операцию ToNumber в разделе 9.3.

Например, true становится 1, false - 0, а undefined превращается в NaN, но (что любопытно) null преобразуется в 0.

ToNumber для string значения, по сути, работает по большей части так же, как правила/синтаксис для числовых литералов (см. Главу 3). В случае неудачи, результатом будет NaN (вместо синтаксической ошибки, как в случае с number-литералами). Один из примеров отличий - в этой операции восьмеричные числа с 0-префиксом обрабатываются не как восьмеричные (с основанием 8) а, как обычные десятичные числа (с основанием 10), хотя такие восьмеричные являются корректным number литералом (см. Главу 2).

Примечание: Различия между грамматикой number-литерала и string-значением в ToNumber тонкие и с многими нюансами, поэтому здесь они не будут рассматриваться. Для получения дополнительной информации обратитесь к разделу 9.3.1 спецификации ES5.

Объекты (и массивы) сначала преобразуются в их эквивалент примитивного значения, а результирующее значение (если оно примитив, но еще не number) преобразуется в number в соответствии с выше описанными правилами ToNumber.

Для преобразования в этот эквивалент примитивного значения, абстрактная операция ToPrimitive (спецификация ES5, раздел 9.1) использует внутреннюю операцию DefaultValue (спецификация ES5, раздел 8.12.8), чтобы определить, есть ли у значения метод valueOf(). Если valueOf() доступен, и он возвращает примитивное значение, то это значение используется для приведения. Если нет, но доступен toString(), он обеспечивает значение для приведения.

Если ни одна из операций не может предоставить примитивное значение, выбрасывается ошибка TypeError.

Начиная с ES5, вы можете создавать такой неприводимый объект - без valueOf() и toString() - если его скрытое свойство [[Prototype]] равно null. Обычно он создаётся с помощью Object.create(null). Обратитесь к книге This и Прототипы Объектов из этой же серии для получения дополнительной информации о [[Prototype]].

Примечание: Позже в этой главе мы подробно рассмотрим, как приводить number, но для следующего фрагмента кода просто предположите, что это выполняет функция Number(..).

Рассмотрим:

var a = {
	valueOf: function(){
		return "42";
	}
};

var b = {
	toString: function(){
		return "42";
	}
};

var c = [4,2];
c.toString = function(){
	return this.join( "" );	// "42"
};

Number( a );			// 42
Number( b );			// 42
Number( c );			// 42
Number( "" );			// 0
Number( [] );			// 0
Number( [ "abc" ] );	// NaN

ToBoolean

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

Прежде всего, в JS есть реальные ключевые слова true и false, и они ведут себя точно так, как вы ожидаете от boolean значений. Распространенное заблуждение, что значения 1 и 0 идентичны true/false. Хотя это может быть верно для других языков, в JS number - это number, а boolean - это boolean. Вы можете привести 1 в true (и наоборот) или 0 в false (и наоборот). Но это не одно и то же.

Ложные значения

Но это еще не конец истории. Нам нужно обсудить, как ведут себя значения, отличные от двух boolean, когда вы приводите их к boolean эквиваленту.

В JavaScript все значения можно разделить на две категории:

  1. значения, которые станут false, если их привести в boolean
  2. все остальное (которые, очевидно, станет true)

Я не шучу. Спецификация JS определяет конкретный, узкий список значений, которые становятся false при приведении в boolean.

Как мы узнаем, что это за список значений? В спецификации ES5 раздел 9.2 определяет абстрактную операцию ToBoolean, которая точно определяет, что происходит для всех возможных значений, когда вы пытаетесь привести их "к булевому (логическому) значению".

Из этой таблицы мы получаем список так называемых "ложных" значений:

  • undefined
  • null
  • false
  • +0, -0 и NaN
  • ""

Это он. Если значение находится в этом списке, это "ложное" значение, и оно будет равно false, если вы приведёте его к boolean.

Согласно логике, если значение отсутствует в этом списке, то оно должно быть в другом списке, который мы называем списком "правдивых" значений. Но JS спецификация на самом деле не определяет "правдивый" список как таковой. Она приводит отдельные примеры, как, например, явное указание, что все объекты - правдивые. Спецификация просто подразумевает: все, что явно не указано в списке ложных, является правдивым.

Ложные объекты

Погодите минутку, название этого раздела даже звучит противоречиво. Я буквально только что сказал, что спецификация называет все объекты правдивыми, верно? Такого понятия, как "ложный объект", не должно быть.

Что бы это вообще могло значить?

У вас может возникнуть соблазн подумать, что это объект-обертка (см. Главу 3) вокруг ложного значения (такого как "", 0 или false). Но не попадайтесь на эту удочку.

Примечание: Это тонкая шутка с уточнением, которую некоторые из вас смогут понять.

Рассмотрим:

var a = new Boolean( false );
var b = new Number( 0 );
var c = new String( "" );

Мы знаем, что все три значения здесь являются объектами (см. Главу 3), обернутыми вокруг явно ложных значений. Но ведут ли себя эти объекты как true или как false? На это легко ответить:

var d = Boolean( a && b && c );

d; // true

Итак, все три ведут себя как true, ибо только так d может оказаться true.

Совет: Обратите внимание на Boolean( .. ), обернутое вокруг выражения a && b && c - вы можете задаться вопросом, зачем оно там. Мы вернемся к этому позже, так что запомните это. Для краткости (с точки зрения мелочей) попробуйте узнать сами, каким будет d, если вы просто выполните d = a && b && c без вызова Boolean( .. )!

Итак, если "ложные объекты" - это не просто объекты, обернутые вокруг ложных значений, то что же это такое, черт возьми?

Сложность заключается в том, что они могут появляться в вашем JS коде, но на самом деле они не являются частью самого JavaScript.

Что!?

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

"Ложный объект" - это значение, которое выглядит и действует как обычный объект (свойства и т.д.). Но, когда вы приводите его значение к boolean, оно превращается в false.

Почему!?

Самым известным примером является document.all: массивоподобный (объект), предоставляемый вашей JS программе DOM-ом (не самим движком JS), который отражает элементы страницы для вашей программы. Он вёл себя как обычный объект - действовал бы он правдиво. Но больше это не так.

document.all само по себе никогда не было по-настоящему "стандартным" и уже давно признано устаревшим и стало неподдерживаемым.

"Не могут ли они просто удалить это тогда?" Извини, хорошая попытка. Жаль, что они не могут. Существует слишком много старого JS кода, который использует его.

Так, зачем заставлять его действовать ложно? Потому что приведение document.all к boolean (например, в операторе if) почти всегда использовалось как средство обнаружения старого, нестандартного IE.

IE уже давно соответствует стандартам и во многих случаях продвигает веб вперед не меньше, а то и больше, чем любой другой браузер. Но весь этот старый код if (document.all) { /* это IE */ } все еще работает, и большая его часть, вероятно, никогда не уйдёт. Всё это наследие из старого кода по-прежнему предполагает, что он работает в IE десятилетней давности, что попросту приводит к некорректной работе страниц у пользователей IE.

Итак, мы не можем удалить document.all полностью, но IE не хочет, чтобы код if (document.all) { .. } больше работал. Он хочет, чтобы пользователи современного IE получали новую, соответствующую стандартам логику кода.

"Что нам делать?" "Я знаю! Давайте уничтожим систему типов JS и притворимся, что document.all - ложный!"

Фу. Это отстой. Это сумасшедшая уловка, которую большинство разработчиков JS не понимают. Но альтернатива (ничего не делать с вышеупомянутыми проблемами) - еще больший отстой.

Итак... вот что мы имеем: сумасшедшие, нестандартные "ложные объекты", добавленные браузерами в JavaScript. Ура!

Правдивые значения

Вернемся к правдивому списку. Какие именно значения правдивые? Помните: значение правдивое, если его нет в списке ложных.

Рассмотрим:

var a = "false";
var b = "0";
var c = "''";

var d = Boolean( a && b && c );

d;

По вашему мнению, какое значение будет иметь d здесь? Это должно быть либо true, либо false.

Это будет true. Почему? Потому что, хотя содержимое этих string выглядит как ложные значения, сами string значения являются истинными, потому что в списке ложных string есть единственное значение - это "".

А эти?

var a = [];				// пустой array -- правдивое или ложное?
var b = {};				// пустой object -- правдивое или ложное?
var c = function(){};	// пустая function -- правдивое или ложное?

var d = Boolean( a && b && c );

d;

Да, вы уже догадались, d здесь по-прежнему true. Почему? Та же причина, что и раньше. Несмотря на то, чем это может казаться, [], {} и function(){} отсутствуют в списке ложных значений и, следовательно, являются истинными значениями.

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

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

Важность правдивости и ложности заключается в понимании того, как будет вести себя значение, если вы его приведёте (явно или неявно) к boolean значению. Теперь, когда у вас есть эти два списка, мы можем углубиться в примеры приведения.

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

Явное приведение относится к преобразованиям типов, которые являются очевидными и явными. Существует широкий спектр применений преобразования типов, которое соответствует категории явного приведения у большинства разработчиков.

Цель заключается в том, чтобы выявить в нашем коде шаблоны - ясного и очевидного преобразования значения из одного типа в другой - так, чтобы не оставлять ям, в которые могут угодить будущие разработчики. Чем более демонстративны мы будем, тем больше вероятность, что кто-то после сможет прочитать и понять наш код без излишних усилий, какие у нас были намерения.

Думается, не будет каких-либо существенных разногласий относительно явного приведения, поскольку оно наиболее точно соответствует общепринятой практике преобразования типов, сложившейся в языках со статической типизацией. Таким образом, мы примем как должное (на данный момент), что явное приведение едино в том, чтобы не быть злонамеренным или противоречивым. Однако мы вернемся к этому позже.

Явно: Строки <--> Числа

Мы начнем с самой простой и, возможно, наиболее распространенной операции приведения: преобразование значений между string и number представлениями.

Для приведения между string и number мы используем встроенные функции String(..) и Number(..) (которые мы назвали "собственными конструкторами" в Главе 3), при этом очень важно, мы не используем ключевое слово new перед ними. Таким образом, мы не создаем объекты-обёртки.

Вместо этого мы на самом деле выполняем явное приведение между двумя этими типами:

var a = 42;
var b = String( a );

var c = "3.14";
var d = Number( c );

b; // "42"
d; // 3.14

String(..) преобразует любое значение в примитивное значение string, используя ранее рассмотренные правила операции ToString. Number(..) преобразует любое значение в примитивное значение number, используя ранее описанные правила операции ToNumber.

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

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

Например, в C/C++ вы можете написать либо (int)x, либо int(x), и оба они преобразуют значение в x в целое число. Обе формы допустимы, но многие предпочитают последнюю, которая выглядит как вызов функции. В JavaScript, когда вы пишете Number(x), это выглядит ужасно похоже. Имеет ли значение, что это на самом деле вызов функции в JS? Не совсем.

Помимо String(..) и Number(..), существуют другие способы "явного" приведения значений между string и number:

var a = 42;
var b = a.toString();

var c = "3.14";
var d = +c;

b; // "42"
d; // 3.14

Вызов a.toString() считается явным (довольно ясно, что "toString" означает "к строке"), но здесь есть определенная скрытая неявность. toString() не может быть вызван для примитивного значения, такого как 42. Поэтому JS автоматически "упаковывает" (см. Главу 3) 42 в объект-обёртку, так что toString() может быть вызван. Другими словами, вы могли бы назвать это "явно неявным".

+c здесь демонстрирует форму унарного оператора + (оператор только с одним операндом). Однако вместо выполнения математического сложения (или сцепления строк - см. ниже) унарный символ + явно приводит свой операнд (c) в значение number.

Является ли +c явным приведением? Зависит от вашего опыта и точки зрения. Если вы знаете (а вы теперь знаете!), что унарный + явно предназначен для приведения к number, то это достаточно явно и очевидно. Однако, если вы никогда не видели этого раньше, это может показаться ужасно запутанным, неявным, со скрытыми побочными эффектами и т.д.

Примечание: Общепринятая точка зрения JS-сообщества с открытым исходным кодом заключается в том, что унарный + является приемлемой формой явного приведения.

Даже если вам действительно нравится форма +c, определенно есть места, где она может выглядеть ужасно запутанной. Рассмотрим пример:

var c = "3.14";
var d = 5+ +c;

d; // 8.14

Унарный оператор - также выполняет приведение, как и +, но он ещё меняет знак числа. При этом вы не можете поместить два оператора рядом с друг другом --, чтобы отменить смену знака, так как это интерпретируется как оператор уменьшения. Вместо этого вам нужно было бы сделать: - -"3.14" с пробелом между ними, тогда бы это вызвало приведение к 3.14.

Вероятно, вы можете придумать всевозможные отвратительные комбинации двоичных операторов (подобно + для сложения) рядом с унарной формой оператора. Вот еще один безумный пример:

1 + - + + + - + 1;	// 2

Вам следует настойчиво избегать приведения с унарным + (или -), когда он примыкает к другим операторам. Хотя пример выше работает, такое почти везде считается плохой идеей. Даже d = +c (или d = + c, если уж на то пошло!) слишком легко спутать с d + = c, что совершенно другое!

Примечание: Другим чрезвычайно запутанным случаем использования унарного + - это помещение его рядом с операторами инкремента ++ и декремента --. Например: a +++b, a + ++b и a + + +b. Больше подробностей о ++ смотрите в разделе "Побочные эффекты выражений" в Главе 5.

Помните, мы пытаемся явно выразить свои намерения и уменьшить путаницу, а не усугублять ее!

Date в number

Другим распространенным случаем использования унарного оператора + является приведение объекта Date в number, так как в результате мы получаем метку времени unix (миллисекунды, прошедшие с 1 января 1970 года 00:00:00 UTC), представляющую значение даты/времени:

var d = new Date( "Mon, 18 Aug 2014 08:53:06 CDT" );

+d; // 1408369986000

Самым распространенным применением этой идиомы является фиксация времени текущего времени в виде метки времени, например, так:

var timestamp = +new Date();

Примечание: Некоторые разработчики знают о специфическом синтаксическом "трюке" в JavaScript, который заключается в том, что () при вызове конструктора (функция, вызываемая с new), являются необязательными, если нет аргументов для передачи. Поэтому, вы можете встретить такой вариант: var timestamp = +new Date;. Однако не все разработчики согласны с тем, что отсутствие () улучшает читаемость, поскольку это необычное синтаксическое исключение применяется только к форме вызова new fn(), а не к обычной форме вызова fn().

Но приведение - это не единственный способ получить метку времени из объекта Date. Вариант без приведения, возможно, даже предпочтительнее, поскольку он более явный:

var timestamp = new Date().getTime();
// var timestamp = (new Date()).getTime();
// var timestamp = (new Date).getTime();

Но еще более предпочтительным вариантом без приведения является использование появившейся в ES5 статической функции Date.now():

var timestamp = Date.now();

В старых браузерах вы можете использовать полифил Date.now(), он довольно простой:

if (!Date.now) {
	Date.now = function() {
		return +new Date();
	};
}

Я бы рекомендовал избегать приведений, связанных с датами. Используйте Date.now() для метки времени текущего момента и new Date( .. ).getTime() для получения метки времени не текущего момента, который можно указать в виде даты/времени.

Любопытный случай с ~

Одним из операторов приведения, который часто упускается из виду и обычно сбивает с толку, является оператор тильды ~ ("побитовое НЕ"). Многие из тех, кто даже понимает, что он делает, часто избегают его. Но, придерживаясь духа этой книги и серии, давайте углубимся в него, чтобы выяснить, может ли ~ дать нам что-нибудь полезное.

В разделе "32-битные целые числа (со знаком)" Главы 2 мы писали, что в JS побитовые операторы работают только как 32-разрядные операторы, то есть они требуют от своих операндов иметь значение в 32-разрядном представлении. Правила того, как это происходит, управляется абстрактной операцией ToInt32 (спецификация ES5, раздел 9.5).

ToInt32 сначала выполняет приведение ToNumber, что означает, что, если значение равно "123", оно сначала станет 123, прежде чем будут применены правила ToInt32.

Хотя технически это не приведение (поскольку тип не меняется!), использование побитовых операторов (такого как | или ~) с определенными специальными значениями number дает эффект приведения и другое значение number.

Например, давайте сначала рассмотрим оператор | - "побитовое ИЛИ", используемый в идиоме 0 | x, которая (как показано в Главе 2) по сути выполняет лишь преобразование ToInt32:

0 | -0;			// 0
0 | NaN;		// 0
0 | Infinity;	// 0
0 | -Infinity;	// 0

Эти специальные числа нельзя представить в 32-разрядном виде (поскольку они взяты из 64-разрядного стандарта IEEE 754 - см. Главу 2), поэтому ToInt32 просто указывает 0 как результат этих значений.

Спорно, является ли 0 | __ явной формой приведения операции ToInt32 или она всё же неявная. С точки зрения спецификации, она безусловно явная, но если вы не понимаете побитовые операции, то она вероятно может показаться неявной и волшебной. Тем не менее, в соответствии с другими утверждениями в этой главе, мы будем называть приведение явным.

Итак, давайте опять обратим наше внимание на ~. Оператор ~ сначала "приводит" к 32-разрядному значению number, а затем выполняет побитовое отрицание (инвертируя каждый бит).

Примечание: Это очень похоже на то, как ! не только преобразует значение в boolean, но и инвертирует его значение (см. обсуждение "унарного !" ниже).

Но... что?! Почему нас волнует, что биты инвертируются? Это довольно специфичная тема. JS-разработчикам довольно редко приходится задумываться об отдельных битах.

Иной смысл сущности ~ даёт информатика старой школы / дискретная математика: здесь ~ - это операция дополнения двойки. Отлично, спасибо, это совершенно ясно!

Давайте попробуем еще раз: ~x примерно совпадает с -(x+1). Это странно, но рассуждать об этом немного легче. Итак:

~42;	// -(42+1) ==> -43

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

Рассмотрим -(x+1). Какое единственное значение, с которым вы можете выполнить эту операцию и получить результат 0 (или -0 технически!)? -1. Другими словами, ~, используемая с разными значениями number, даст ложное значение 0 (легко приводимое к false) для входного значения -1. В любом другом случае мы значение number будет правдивым.

Какое отношение это имеет к делу?

-1 обычно зовётся "контрольным значением" - это значение, которому придается определенный семантический смысл в наборе данных того же типа (number). Язык C использует контрольные значения -1 во многих функциях, которые возвращают значения >= 0 в случае "успеха" и -1 для "неудач".

JavaScript заимствовал этот подход для string операции indexOf(..), которая ищет подстроку и, если найдена, возвращает ее индекс-позицию или -1, если она не найдена.

Довольно часто пытаются использовать indexOf(..) не только для поиска позиции, но и как boolean проверку наличия/отсутствия подстроки в другой string. Вот как обычно выполняют такие проверки:

var a = "Hello World";

if (a.indexOf( "lo" ) >= 0) {	// true
	// нашёл!
}
if (a.indexOf( "lo" ) != -1) {	// true
	// нашёл
}

if (a.indexOf( "ol" ) < 0) {	// true
	// не нашёл!
}
if (a.indexOf( "ol" ) == -1) {	// true
	// не нашёл!
}

Я нахожу довольно неэстетичным видеть в коде >= 0 или == -1. По сути, это "дырявая абстракция", поскольку она выставляет наружу, в мой код, свою внутреннюю реализацию - использование контрольного -1 для "сбоя". Я бы предпочел, чтобы такие детали были скрыты.

Наконец, мы видим, как ~ могла бы нам помочь! Использование ~ с indexOf() "приводит" (на самом деле просто преобразует) результат к значению, которое можно привести к boolean:

var a = "Hello World";

~a.indexOf( "lo" );			// -4   <-- правдивое!

if (~a.indexOf( "lo" )) {	// true
	// нашёл!
}

~a.indexOf( "ol" );			// 0    <-- ложное!
!~a.indexOf( "ol" );		// true

if (!~a.indexOf( "ol" )) {	// true
	// не нашёл!
}

~ принимает возвращаемое значение indexOf(..) и преобразует его: для -1 "сбоя" мы получаем ложный 0, а все остальные значения являются правдивыми.

Примечание: Псевдоалгоритм -(x + 1) для ~ подразумевает, что ~-1 равно -0, но на самом деле тильда выдает 0, потому что операция на самом деле побитовая, а не математическая.

Технически, if (~a.indexOf(..)) использует неявное приведение результирующего значения 0 к false или ненулевого значения к true. Но в целом, мне кажется, ~ больше похожа на явное приведение, если вы знаете, цель этого оператора в этой идиоме.

Я считаю, что это более ясный код, чем беспорядок с >= 0 / == -1.

Отбрасывание битов

Ещё одно место, где ~ может появиться в коде, с которым вы сталкиваетесь: некоторые разработчики используют двойную тильду ~~, чтобы отбросить десятичную часть number (т.е. "привести" его к целому числу "integer"). Часто утверждается (хотя и ошибочно), что это тот же самое, что и вызов Math.floor(..).

Как работает ~~: первая ~ выполняет "приведение" ToInt32 и побитовую инверсию, затем вторая ~ выполняет еще одно побитовое переключение, возвращая все биты обратно в исходное состояние. Итог - просто "приведение" ToInt32 (оно же усечение/отбрасывание).

Примечание: Побитовая двойная инверсия ~~ очень похожа на действие двойного отрицания !!, которое рассматривается ниже в разделе "Явно: * --> Булево значение".

Однако ~~ нуждается в некотором предостережении/разъяснении. Во-первых, он корректно работает только с 32-разрядными значениями. Но что еще более важно, с отрицательными числами она действует не так, как Math.floor (..)!

Math.floor( -49.6 );	// -50
~~-49.6;				// -49

Оставляя в стороне отличие Math.floor(..), ~~x усекает до (32-разрядного) целого числа. Но то же самое делает и x | 0, и, по-видимому, с (немного) меньшими усилиями.

Итак, почему же тогда вы могли бы выбрать ~~x вместо x | 0? Приоритет операторов (см. Главу 5):

~~1E20 / 10;		// 166199296

1E20 | 0 / 10;		// 1661992960
(1E20 | 0) / 10;	// 166199296

Как и во всех других советах здесь, используйте ~ и ~~ в качестве явного механизма "приведения" и преобразования значений только в том случае, если каждый, кто читает/пишет такой код, в курсе, как работают эти операторы!

Явно: Парсинг числовых строк

Аналогичный приведению string к number результат можно получить путем выявления чисел в символах строки. Однако есть явные различия между таким парсингом и преобразованием типов, рассмотренным ранее.

Взгляните:

var a = "42";
var b = "42px";

Number( a );	// 42
parseInt( a );	// 42

Number( b );	// NaN
parseInt( b );	// 42

Парсинг числового значения из строки терпим к нечисловым символам - он просто прекращает синтаксический анализ слева направо при их обнаружении. Приведение же не терпимо и приводит к значению NaN.

Парсинг не следует рассматривать как замену приведению. Эти две задачи, хотя и схожи, имеют разные цели. Разбирайте string как number, когда вы не знаете / неважно, появятся ли нечисловые символы с правой стороны. Приводите stringnumber), когда допустимы лишь числовые значения, а что-то вроде "42px" должно быть отклонено.

Совет: parseInt(..) имеет двойника, parseFloat(..), который (как понятно) извлекает из строки число с плавающей запятой.

Не забывайте, что parseInt(..) работает со значениями string. Нет абсолютно никакого смысла передавать значение number в parseInt(..). Также не имеет смысла передавать значение любого другого типа, например, true, function(){..} или [1,2,3].

Если вы передаете значение, отличное от string, то оно сначала автоматически приводится в string (см. "ToString" выше), что очевидно своего рода скрытое неявное приведение. Это действительно плохая идея полагаться на такое поведение в вашей программе, поэтому никогда не используйте parseInt(..) со значением, отличным от string.

До ES5 была другая ошибка с parseInt(..), которая служила источником ошибок во многих программах на JS. Если вы не передавали второй аргумент (radix), чтобы обозначить, какую систему счисления использовать для интерпретации строки, parseInt(..) анализировала первые символы строки и делала предположения.

Если первые два символа "0x" или "0X", предполагалось (по соглашению), что вы хотели интерпретировать string как шестнадцатеричное number (основание - 16). Иначе, если первый символ "0", предполагалось (опять же, по соглашению), что вы хотели интерпретировать string как восьмеричное number (основание - 8).

Шестнадцатеричную строку (с начальным значением 0x или 0X) не так-то просто перепутать. Но угадывание восьмеричных чисел оказалось дьявольским явлением. Например:

var hour = parseInt( selectedHour.value );
var minute = parseInt( selectedMinute.value );

console.log( "The time you selected was: " + hour + ":" + minute);

Выглядит безобидным, правда? Попробуйте взять 08 для часа и 09 для минуты. Вы получите 0:0. Почему? потому что ни 8, ни 9 не являются допустимыми символами в восьмеричной системе.

До ES5 заплатка была простой, но её так легко забыть: всегда передавайте 10 в качестве второго аргумента. Это было абсолютно безопасно:

var hour = parseInt( selectedHour.value, 10 );
var minute = parseInt( selectedMiniute.value, 10 );

Начиная с ES5, parseInt(..) больше не угадывает восьмеричные числа. Если вы не скажете обратное, он предполагает базу - 10 (или базу - 16 для префиксов "0x"). Это гораздо приятнее. Просто будьте осторожны, если ваш код должен выполняться в окружении до ES5.В этом случае вам нужно передать 10 в radix (основание).

Парсинг не строк

Один печально известный пример поведения parseInt(..) был опубликован в саркастически-шутливом посте несколько лет назад:

parseInt( 1/0, 19 ); // 18

Предположительное (но совершенно неверное) утверждение гласило: "Если я передам Infinity и разберу её в целое число, то я должен получить Infinity, а не 18". Вероятно, JS совсем без ума раз дает такой результат, верно?

Хотя этот пример явно надуман и нереален, давайте на мгновение предадимся безумию и посмотрим, действительно ли JS настолько дурацкий.

Во-первых, самый очевидный грех, совершенный здесь, заключается в передаче не-string в parseInt(..). Этого не должно быть. Делая так, ты напрашиваешься на неприятности. Но даже совершая такое, JS вежливо приведет то, что вы передаете, в string, которую он затем может попытаться распарсить.

Некоторые могут возразить, что это неоправданное поведение, и что parseInt(..) должен отказаться работать со значением, отличным от string. Возможно, это должно выдавать ошибку? Честно говоря, это было бы очень похоже на Java. Я содрогаюсь при мысли, что JS должен начать выдавать ошибки повсюду, так что try..catch требуется почти в каждой строке.

Должен ли он возвращать NaN? Может быть. Но... как насчет:

parseInt( new String( "42") );

Потерпит ли это тоже неудачу? Это значение, отличное от string. Если вы хотите, чтобы объект-обёртка String был распакован в "42", то действительно ли так необычно, что 42 сначала становится "42", чтобы потом превратиться в 42?

Я бы сказал, что это наполовину явное и наполовину неявное приведение, которое может иметь место, и может быть очень полезным. Например:

var a = {
	num: 21,
	toString: function() { return String( this.num * 2 ); }
};

parseInt( a ); // 42

Тот, что parseInt(..) принудительно приводит значение к string перед парсингом, вполне разумно. Если вы кладёте мусор и вынимаете его обратно, не вините мусорное ведро - оно просто добросовестно выполняло свою работу.

Итак, если вы передаете значение типа Infinity (очевидный результат 1 / 0), какое string-представление имеет наибольший смысл для его дальнейшего приведения? На ум приходят только два разумных варианта: "Infinity" и "∞". JS выбрал "Infinity" и я рад, что он так поступил.

Я думаю, это хорошо, что все значения в JS имеют какое-то string представление по умолчанию, поэтому они не таинственные черные ящики, которые мы не можем отлаживать и анализировать их.

Хорошо, что насчет основания 19? Очевидно, совершенно фальшивая и надуманная. Никакие реальные JS-программы не используют основание 19. Это абсурд. Но опять же, давайте предадимся нелепице. В системе с основанием 19 допустимыми числовыми символами являются 0 - 9 и a - i (без учета регистра).

Итак, вернемся к нашему примеру parseInt( 1/0, 19 ). По сути, это parseInt( "Infinity", 19 ). Как это анализируется? Первый символ - "I" - в глупой девятнадцатеричной системе имеет значение 18. Второй символ "n" в допустимом наборе числовых символов отсутствует, и поэтому анализ просто вежливо останавливается, точно так же, как когда он наткнулся на "p" в "42px".

Результат? 18. Получилось именно так, как и должно быть. Логика привела нас сюда, а не к ошибке или к Infinity, что очень важно для JS, и поэтому его не следует так легко отбрасывать.

Приведём ещё примеры такого поведения parseInt(..), которые могут показаться удивительными, но которые вполне разумные:

parseInt( 0.000008 );		// 0   ("0" из "0.000008")
parseInt( 0.0000008 );		// 8   ("8" из "8e-7")
parseInt( false, 16 );		// 250 ("fa" из "false")
parseInt( parseInt, 16 );	// 15  ("f" из "function..")

parseInt( "0x10" );			// 16
parseInt( "103", 2 );		// 2

parseInt(..) на самом деле довольно предсказуем и последователен в своем поведении. Если вы будете использовать его правильно, вы получите разумные результаты. Если вы используете его неправильно, сумасшедшие результаты, которые вы получаете, не являются ошибкой JavaScript.

Явно: * --> Булево значение

Теперь давайте рассмотрим приведение любого значения, не являющегося boolean, к boolean.

Точно так же, как со String(..) и Number(..) выше, Boolean(..) (без new, конечно!) - это явный способ приведения ToBoolean.:

var a = "0";
var b = [];
var c = {};

var d = "";
var e = 0;
var f = null;
var g;

Boolean( a ); // true
Boolean( b ); // true
Boolean( c ); // true

Boolean( d ); // false
Boolean( e ); // false
Boolean( f ); // false
Boolean( g ); // false

Хотя Boolean (..) явно выражает намерение, он мало распространен или не идиоматичен.

Точно так же, как унарный оператор + преобразует значение в number (см. выше), унарный оператор отрицания ! явно преобразует значение в boolean. Проблема заключается в том, что он также меняет значение с истинного на ложное или наоборот. Поэтому, самый популярный способ явного приведения к boolean, к которому прибегают JS разработчики, - это оператор двойного отрицания !!. В нём второй ! вернет исходный true/false:

var a = "0";
var b = [];
var c = {};

var d = "";
var e = 0;
var f = null;
var g;

!!a;	// true
!!b;	// true
!!c;	// true

!!d;	// false
!!e;	// false
!!f;	// false
!!g;	// false

Любое приведение ToBoolean выполняется неявно без Boolean(..) или !!, если используется в boolean контексте, таком как оператор if (..) ... Но наша цель - это явное приведение значения к boolean, чтобы было понятно, что мы используем намеренное приведение ToBoolean.

Другой пример использования явного приведения ToBoolean - это, когда вы хотите выполнить приведение к true/false при сериализации структуры данных в формате JSON:

var a = [
	1,
	function(){ /*..*/ },
	2,
	function(){ /*..*/ }
];

JSON.stringify( a ); // "[1,null,2,null]"

JSON.stringify( a, function(key,val){
	if (typeof val == "function") {
		// выполнить приведение `ToBoolean` для функции
		return !!val;
	}
	else {
		return val;
	}
} );
// "[1,true,2,true]"

Если вы пришли в JavaScript из Java, вы можете узнать эту идиому:

var a = 42;

var b = a ? true : false;

Тернарный оператор ? : проверит a на правдивость и на основе этого присвоит b либо true, либо false.

На первый взгляд эта идиома выглядит как форма явного приведения ToBoolean, поскольку, очевидно, операция выдает true или false.

Однако присутствует скрытое неявное приведение, заключающееся в том, что выражение a сначала должно быть приведено в boolean для прохождения теста на правдивость. Я бы назвал эту идиому "явно неявной". Кроме того, я бы посоветовал вам всегда избегайте эту идиому в JavaScript. Это не дает никакой пользы, но, что куда хуже, выдает себя за то, чем оно не является.

Boolean(a) и !!a намного лучше в качестве явных вариантов приведения.

Неявное приведение

Неявное приведение относится к скрытым преобразованиям типов с неочевидными побочными эффектами, которые неявно возникают в результате других действий. Другими словами, неявные приведения - это любые преобразования типов, которые не очевидны (для вас).

Если цель явного приведения ясна (сделать код однозначным и более понятным), то также понятно, что неявное приведение преследует противоположную цель: усложнить понимание кода.

Если принять всё за чистую монету, то, я полагаю, это источник большей части гнева по отношению к приведению. Большинство жалоб на "приведение JavaScript" на самом деле касаются (осознано или нет) неявного приведения.

Примечание: Дуглас Крокфорд, автор книги "JavaScript: сильные стороны", утверждал во многих выступлениях на конференциях и в статьях, что следует избегать приведения в JavaScript. Но что он, по-видимому, имеет в виду, так это то, что неявное приведение - это плохо (по его мнению). Однако, если вы прочтете его собственный код, вы найдете множество примеров приведения, как неявного, так и явного! По правде говоря, его беспокойство, похоже, в первую очередь направлено на операцию ==, но, как вы увидите в этой главе, это только часть механизма приведения.

Итак, является ли скрытое приведение злом? Это опасно? Является ли это недостатком в дизайне JavaScript? Должны ли мы избегать этого любой ценой?

Бьюсь об заклад, большинство из вас, читатели, склонны с энтузиазмом воскликнуть: "Да!"

Не так быстро. Выслушай меня.

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

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

Упрощение неявного

Прежде чем мы перейдем к JavaScript, позвольте мне предложить что-нибудь псевдокодовое из какого-нибудь теоретического строго типизированного языка для иллюстрации:

SomeType x = SomeType( AnotherType( y ) )

В этом примере у меня есть некоторый произвольный тип значения в y, который я хочу преобразовать в тип SomeType. Проблема в том, что этот язык не может перейти непосредственно от того, чем в данный момент является y, к SomeType. Ему нужен промежуточный шаг, на котором он сначала преобразуется в AnotherType, а затем из AnotherType в SomeType.

Теперь, что, если бы этот язык просто позволил вам написать:

SomeType x = SomeType( y )

Разве вы в целом не согласились бы с тем, что мы упростили преобразование типов, чтобы уменьшить ненужный "шум" промежуточного этапа преобразования? Я имею в виду, действительно ли так важно, прямо здесь, на этом этапе кода, увидеть и смириться с тем фактом, что y сначала переходит в AnotherType, а затем в SomeType?

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

Несомненно, где-то за кулисами все еще происходит промежуточный этап преобразования. Но если здесь эта деталь скрыта от глаз, мы можем просто рассуждать о том, что преобразуем y в SomeType в виде стандартной операции, скрывая ненужные детали.

Хотя это и не идеальная аналогия, я собираюсь утверждать на протяжении всей остальной части этой главы, что неявное приведение JS можно рассматривать как оказание аналогичной помощи вашему коду.

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

Многие разработчики считают, что если механизм может делать какую-то полезную вещь A, но также может быть использован не по назначению для совершения какой-то ужасной вещи Z, то мы должны полностью отказаться от него, просто на всякий случай.

Мой совет вам таков: не соглашайтесь на это. Не "выплескивайте ребенка вместе с водой". Не думайте, что неявное приведение - это плохо, потому что все, что, по вашему мнению, вы когда-либо видели, - это его "плохие стороны". Я думаю, что здесь есть "сильные стороны", и я хочу помочь и вдохновить большинство из вас найти и принять их!

Неявно: Строки <--> Числа

Ранее в этой главе мы исследовали явное приведение между значениями string и number. Теперь давайте рассмотрим ту же задачу, но с использованием методов неявного приведения. Но прежде чем мы это сделаем, мы должны изучить некоторые нюансы операций, которые неявно приведут к приведению.

Оператор + очень занятой. Он и складывает number, и сцепляет string. Итак, как JS догадывается, какой тип операции вы хотите использовать? Взгляните:

var a = "42";
var b = "0";

var c = 42;
var d = 0;

a + b; // "420"
c + d; // 42

Где та разница, которая формирует либо "420", либо 42? Распространенное заблуждение, что разница в том, является ли один или оба операнда string, поскольку это означает, что + предполагает сцепление string. Хотя это отчасти верно, но все гораздо сложнее.

Рассмотрим:

var a = [1,2];
var b = [3,4];

a + b; // "1,23,4"

Ни один из этих операндов не является string, но очевидно, что они оба были приведены к string, а затем произошло сцепление string. Так что же происходит на самом деле?

(Предупреждение: грядет очень подробный разбор спецификации, так что пропустите следующие два абзаца, если это вас пугает!)


Согласно разделу 11.6.1 спецификации ES5, алгоритм + (когда object-значение является операндом) будет сцеплять, если любой из операндов либо уже является string, либо если следующие шаги ведут к string. Итак, когда + получает object (включая array) для любого из операндов, он для значения сначала вызывает абстрактную операцию ToPrimitive (раздел 9.1), которая затем вызывает алгоритм [[DefaultValue]] (раздел 8.12.8) с контекстной подсказкой number.

Если вы будете внимательны, то заметите, что эта операция теперь идентична тому, как абстрактная операция ToNumber обрабатывает object (см. раздел "ToNumber"" выше). Операция value Of() над array не приведет к созданию простого примитива, поэтому затем он переходит к toString(). Так, два массива становятся "1,2" и "3,4" соответственно. Теперь + сцепляет две string так, как вы ожидаете: "1,23,4".


Давайте оставим в стороне эти запутанные детали и вернемся к более раннему, упрощенному объяснению: если любой из операндов + является string (или становится таковым с помощью описанных выше шагов!), операция будет сцеплением string. В противном случае это всегда числовое сложение.

Примечание: Часто цитируемый пример приведения - [] + {} против {} + [], которые приводят, соответственно, к "[object Object]" и 0. Однако это еще не все, и мы рассмотрим эти детали в разделе "Блоки" в Главе 5.

Что это значит для неявного приведения?

Вы можете привести number к string, просто "добавив" number и "" - пустую string:

var a = 42;
var b = a + "";

b; // "42"

Совет: Числовое сложение с помощью оператора + является коммутативным, что означает, что 2 + 3 совпадает с 3 + 2. Объединение строк с помощью +, очевидно, обычно не является коммутативным, но в конкретном случае "" оно эффективно коммутативно, так как a + "" и "" + a приведут к одному и тому же результату.

Чрезвычайно популярно / идиоматично (*неявно *) приводить number к string с помощью операции + "". Интересно, что даже некоторые из самых ярых критиков неявного приведения все еще используют этот подход в своем собственном коде вместо одной из его явных альтернатив.

Я думаю, что это отличный пример полезной формы в неявном приведении, несмотря на то, как часто этот способ подвергается критике!

Сравнивая неявное приведение a + "" с предыдущим примером явного приведения String(a), необходимо учитывать еще одну особенность. Следуя логике работы абстрактной операции ToPrimitive, сначала a + "" вызывает valueOf() для значения a, затем возвращаемое значение преобразуется в string с помощью внутренней абстрактной операции ToString. В то же время String(a) просто вызывает toString() напрямую.

Оба способа в конечном итоге приводят к string, но если вы используете object вместо обычного примитивного number, вы не обязательно можете получить такое же значение string!

Рассмотрим:

var a = {
	valueOf: function() { return 42; },
	toString: function() { return 4; }
};

a + "";			// "42"

String( a );	// "4"

Как правило, такого рода уловки вам не вредят, если вы только действительно не пытаетесь создавать запутанные структуры данных и методы, но вам следует быть осторожным, если вы пишете свои собственные методы valueOf() и toString() для какого-то object, поскольку то, как вы приводите значение, может повлиять на результат.

А как насчет другого направления? Как мы можем неявно привести из string в number?

var a = "3.14";
var b = a - 0;

b; // 3.14

Оператор - определен только для числового вычитания, поэтому a - 0 вызывает приведение значения a к number. Куда менее распространенные, a * 1 и a / 1 также приводят к тому же результату, ибо эти операторы предназначены только для числовых операций.

Как насчет значений object с оператором -? Аналогичная история, как и для + выше:

var a = [3];
var b = [1];

a - b; // 2

Оба значения array должны стать number, но сначала они преобразуются в strings (с использованием ожидаемой сериализации toString()), а затем преобразуются в number для выполнения вычитания -.

Итак, является ли неявное приведение к string и number тем уродливым злом, о котором вы слышите в страшилках? Лично я так не думаю.

Сравните b = String(a) (явный) с b = a + "" (неявный). Я думаю, что можно привести примеры того, что оба подхода полезны в вашем коде. Конечно, b = a + "" немного чаще встречается в JS программах, демонстрируя свою полезность, невзирая на чувства по поводу достоинств или опасностей неявного приведения в целом.

Неявно: Булевы значения --> Числа

По-моему, ситуация, когда неявное приведение может проявить свои лучшие стороны, - это случай упрощения определенных типов сложной boolean логики в виде простого числового сложения. Конечно, это не техника общего назначения, а конкретное решение для особых случаев.

Рассмотрим:

function onlyOne(a,b,c) {
	return !!((a && !b && !c) ||
		(!a && b && !c) || (!a && !b && c));
}

var a = true;
var b = false;

onlyOne( a, b, b );	// true
onlyOne( b, a, b );	// true

onlyOne( a, b, a );	// false

Функция onlyOne(..) должна возвращать true только в том случае, если лишь один из её аргументов является true / правдивым. Она использует неявное приведение в проверках на правдивость и явное приведение в остальных случаях, включая возвращаемое итоговое значение.

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

Вот где приведение boolean значений к number (очевидно, 0 или 1) может сильно помочь:

function onlyOne() {
	var sum = 0;
	for (var i=0; i < arguments.length; i++) {
        // пропускаем ложные значения,
		// считая их 0, но избегайте NaN.
		if (arguments[i]) {
			sum += arguments[i];
		}
	}
	return sum == 1;
}

var a = true;
var b = false;

onlyOne( b, a );				// true
onlyOne( b, a, b, b, b );		// true

onlyOne( b, b );				// false
onlyOne( b, a, b, b, b, a );	// false

Примечание: Конечно, вместо цикла for в onlyOne(..) вы могли бы более лаконично использовать функцию ES5 reduce(..), но я не хотел затуманивать концепцию.

То, что мы здесь делаем, - это полагаемся на 1 для true/правдивых приведений и численно складываем их. sum + = arguments[i] использует неявное приведение, чтобы это произошло. Если одно и только одно значение в списке arguments равно true, то числовая сумма будет равна 1, в противном случае сумма не будет равна 1 и, таким образом, желаемое условие не выполняется.

Конечно, мы могли бы сделать тоже самое с помощью явного приведения:

function onlyOne() {
	var sum = 0;
	for (var i=0; i < arguments.length; i++) {
		sum += Number( !!arguments[i] );
	}
	return sum === 1;
}

Мы сначала используем !!arguments[i], побуждая привести значения к true или false. Это для того, чтобы вы могли передавать значения, отличные от boolean, например, onlyOne( "42", 0 ), и утилита работала бы так, как ожидается (в противном случае вы получили бы сцепление string, и логика была бы неверной).

После того, как мы чётко знаем, что это boolean, мы выполняем еще одно явное приведение с помощью Number(..), чтобы получить 0 или 1.

Является ли явная форма приведения в этой утилите "лучше"? Она позволяет избежать ловушки NaN, как описано в комментариях к коду. Но, в конечном счете, это зависит от ваших потребностей. Лично я считаю, что первая версия, опирающаяся на неявное приведение, более элегантна (если вы не будете передавать undefined или NaN), а явная версия излишне многословна.

Но, как и почти во всем, то, что мы здесь обсуждаем, это субъективные решения.

Примечание: Независимо от подхода, явного или неявного, вы можете легко создать варианты onlyTwo(..) или onlyFive(..), просто изменив итоговое сравнение с 1 на 2 или 5 соответственно. Это значительно проще, чем добавлять кучу выражений && и ||. Так что, в общем, приведение в данном случае очень полезно.

Неявно: * --> Булево значение

Теперь давайте обратим наше внимание на неявное приведение к boolean значениям, поскольку оно, безусловно, самое распространенное, а также всё ещё самое потенциально проблемное.

Помните, неявное приведение - это то, что возникает, когда вы используете значение так, что приходится преобразовать значение. Для number и string операций довольно легко увидеть ситуации вынужденного приведения.

Но какие операции требуют/заставляют (неявно) приводить к boolean?

  1. Проверяемое выражение в операторе if (..).
  2. Проверяемое выражение (второе) в for ( .. ; .. ; .. ).
  3. Проверяемое выражение в циклах while (..) и do..while(..).
  4. Проверяемое выражение (первое) в тернарном операторе ? :.
  5. Левый операнд (является проверяемым выражением - см. ниже!) в операторах || ("логическое или") и && ("логическое и").

Любое значение, используемое в этих контекстах, которое еще не является boolean, будет неявно преобразовано в boolean с использованием правил абстрактной операции ToBoolean, рассмотренной выше в этой главе.

Давайте взглянем на примеры:

var a = 42;
var b = "abc";
var c;
var d = null;

if (a) {
	console.log( "угу" );		// угу
}

while (c) {
	console.log( "нет, никогда не выполнится" );
}

c = d ? a : b;
c;								// "abc"

if ((a && d) || c) {
	console.log( "угу" );		// угу
}

Во всех этих контекстах значения, не являющиеся boolean, неявно приводятся к их boolean эквивалентам для выполнения проверки.

Операторы || и &&

Вероятно, вы видели операторы || ("логическое или") и && ("логическое и") в большинстве или во всех других языках, которые вы использовали. Поэтому было бы естественно предположить, что в JavaScript они работают в основном так же, как и в других подобных языках.

Есть очень малоизвестный, но очень важный нюанс.

На самом деле, я бы сказал, что эти операторы даже не следует называть "логическими ___ операторами", поскольку такое название неполно описывает то, что они делают. Если бы я давал им более точное (хотя и более неуклюжее) название, я бы назвал их "операторами выбора" или, более полно, "операторами выбора операндов".

Почему? Потому что в JavaScript они на самом деле не приводят к логическому значению (boolean), как это происходит в некоторых других языках.

Так, к чему они приводят? Они приводят к значению одного (и только одного) из их двух операндов. Другими словами, они выбирают одно из значений двух операндов.

Цитирую раздел 11.11 спецификации ES5:

Значение, создаваемое оператором && или ||, не обязательно имеет тип Boolean. Полученное значение всегда будет значением одного из выражений двух операндов.

Давайте проиллюстрирую:

var a = 42;
var b = "abc";
var c = null;

a || b;		// 42
a && b;		// "abc"

c || b;		// "abc"
c && b;		// null

Подождите, что!? Подумайте об этом. В таких языках, как C и PHP, эти выражения приводят к true или false, но в JS (и Python, и Ruby, если уж на то пошло!) результат определяется самими значениями.

Оба оператора || и && выполняют boolean тест для первого операнда (a или c). Если операнд еще не является boolean (а здесь это не так), происходит обычное приведение ToBoolean, и проверка может быть выполнена.

Для оператора ||, если итог проверки true, выражение || возвращает значение первого операнда (a или c). Если итог проверки false, выражение || возвращает значение второго операнда (b).

И наоборот для оператора &&. Если итог проверки true, выражение && возвращает значение второго операнда (b). Если итог проверки false, выражение && возвращает значение первого операнда (a или c).

Результатом выражения || или && всегда является значение одного из операндов, а не результат проверки (возможно, приведенный). В c && b, c равно null и, следовательно, является ложным. Но само выражение && возвращает null (значение c), а не приведённый false, использованный при проверке.

Теперь вы понимаете, что эти операторы действуют как "селекторы операндов"?

Другой взгляд на эти операторы:

a || b;
// примерно эквивалентно:
a ? a : b;

a && b;
// примерно эквивалентно:
a ? b : a;

Примечание: Я говорю a || b "примерно эквивалентно" a ? a : b потому что результат идентичен, но есть определённая разница. В а ? a : b, если бы a было более сложным выражением (например, которое могло бы иметь побочные эффекты, такие как вызов function и т.д.), то выражение a, возможно, было бы вычислено дважды (если первая оценка была правдивой). В a || b, напротив, выражение a вычисляется лишь один раз, а это значение используется и для проверки, и в качестве результата (если требуется). Тот же нюанс и в выражениях a && b и a ? b : a.

Чрезвычайно распространенным и полезным применением данного поведения, которое вы, вероятно, уже использовали и не до конца поняли, следующее:

function foo(a,b) {
	a = a || "hello";
	b = b || "world";

	console.log( a + " " + b );
}

foo();					// "hello world"
foo( "yeah", "yeah!" );	// "yeah yeah!"

Идиома a = a || "hello" (иногда говорят, что это JavaScript-версия "оператора нулевого слияния" из C#) проверяет a и, если у переменной нет значения (или оно нежелательное ложное), то возвращает резервное значение по умолчанию ("hello").

Но будь осторожен!

foo( "That's it!", "" ); // "That's it! world" <-- Ой!

Видите проблему? "" во втором аргументе - это ложное значение (см. ToBoolean ранее в этой главе). Поэтому проверка в b = b || "world" терпит неудачу и замещается значением по умолчанию "world", хотя цель, вероятно, состояла в том, чтобы явно переданная пустая строка "" была присвоена b.

Эта идиома || чрезвычайно распространена и весьма полезна, но вы должны использовать ее только в тех случаях, когда все ложные значения должны быть проигнорированы. В противном случае вам следует быть более точным в своей проверке и, возможно, прибегнуть к тернарному оператору ? :.

Эта идиома присвоения значения по умолчанию настолько распространена (и полезна!), что даже те, кто публично и яростно осуждает приведение в JavaScript, часто используют ее в своем собственном коде!

А как насчет &&?

Есть еще одна идиома, которая несколько реже создается вручную, но которая часто используется JS минификаторами. Оператор && "выбирает" второй операнд тогда и только тогда, когда первый операнд признаётся правдивым, и этот подход иногда называют "защитным оператором" (также см. "Короткое замыкание" в Главе 5) - проверка первого выражения "защищает" второе выражение:

function foo() {
	console.log( a );
}

var a = 42;

a && foo(); // 42

foo() вызывается только потому, что a определяется, как правдивый. Если результат проверки ложный, то выполнение оператора в выражении a && foo() молча остановится - это называют "коротким замыканием" - и foo() никогда не будет вызван.

Опять же, люди не так часто пишут подобные вещи. Обычно вместо этого они выбирают if (a) { foo(); }. Но минификаторы JS предпочитают a && foo(), потому что это намного короче. Итак, теперь, если вам когда-нибудь придется расшифровывать такой код, вы будете знать, что он делает и почему.

Хорошо, итак, у || и && есть несколько изящных трюков в рукаве, если вы готовы допустить неявное приведение в своей работе.

Примечание: Обе идиомы a = b || "something" и a && b() опираются на короткое замыкание, которое мы более подробно рассмотрим в Главе 5.

Тот факт, что эти операторы на самом деле не приводят к true и false, возможно, уже немного заморочил вам голову. И вы, вероятно, удивляетесь, как вообще работали все ваши операторы if и циклы for, если в них были сложносоставные логические выражения, такие как a && (b || c).

Не волнуйся! Мир не рушится. С вашим кодом (возможно) всё хорошо. Просто вы, наверное, никогда раньше не осознавали, что после того, как было вычислено составное выражение, происходило неявное приведение к boolean.

Взгляните:

var a = 42;
var b = null;
var c = "foo";

if (a && (b || c)) {
	console.log( "yep" );
}

Этот код по-прежнему работает так, как вы всегда думали, за исключением одной незначительной дополнительной детали. Выражение a && (b || c) на самом деле возвращает "foo", а не true. Итак, оператор if затем приводит значение foo к boolean, которое, конечно же, будет true.

Видите? Нет причин для паники. Ваш код, вероятно, все еще в безопасности. Но теперь вы знаете больше о том, как он делает то, что делает.

И теперь вы также понимаете, что такой код использует неявное приведение. Если вы все еще в лагере "избегайте (неявного) приведения", вам нужно будет вернуться к своему коду и сделать все эти проверки явными:

if (!!a && (!!b || !!c)) {
	console.log( "yep" );
}

Удачи вам в этом! ... Извините, просто подтруниваю.

Приведение Symbol

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

Но Symbol из ES6 вносят подвох в систему приведения, который нам нужно кратко обсудить. По причинам, которые выходят далеко за рамки того, что мы обсуждаем в этой книге, допускается явное преобразование symbol в string, но неявное приведение того же самого запрещено и выкидывает ошибку.

Рассмотрим:

var s1 = Symbol( "cool" );
String( s1 );					// "Symbol(cool)"

var s2 = Symbol( "not cool" );
s2 + "";						// TypeError

Значения symbol вообще нельзя привести к number (всегда выдает ошибку), но, как ни странно, их можно как явно, так и неявно приводить к boolean (всегда true).

Согласованной системе всегда легче научиться, а с исключениями всегда невесело иметь дело. Нам просто нужно быть осторожными с новыми symbol из ES6 и с тем, как их приводить.

Хорошая новость: вероятно, вам крайне редко понадобится приводить значение symbol. То, как они обычно используются (см. Главу 3), не требует их приведения на постоянной основе.

Обычное равенство vs. Строгое равенство

Обычное равенство - это оператор ==, а строгое равенство - это оператор ===. Оба оператора используются для сравнения двух значений на "равенство". Это размежевание на "обычное" и "строгое" указывает на очень важную разницу в поведении между ними, в частности, в том, как они определяют "равенство".

Очень распространенное заблуждение относительно этих двух операторов таково: "== проверяет значения на равенство, а === проверяет как значения, так и типы на равенство". Хотя это звучит красиво и разумно, но неточно. Бесчисленные уважаемые книги и блоги по JavaScript говорят именно об этом, но, к сожалению, все они неправы.

Правильное описание таково: "== допускает приведение при сравнении на равенство, а === запрещает приведение".

Быстродействие равенства

Остановитесь и подумайте о разнице между первым (неточным) и этим вторым (точным) объяснением.

В первом объяснении кажется очевидным, что === выполняет больше работы, чем ==, потому что он должен также проверять тип. Во втором объяснении == - это то, что выполняет больше работы, потому что он должен выполнять приведение, если типы разные.

Не попадайтесь в эту ловушку, думая как большинство, что это как-то связано с производительностью - будто бы == медленнее, чем ===. Действительно это можно замерить и увидеть, что приведение действительно занимает некоторое время, но это всего лишь микросекунды (да, это миллионные доли секунды!).

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

Если вы сравниваете два значения разных типов, производительность не является важным фактором. Что вы должны спросить себя: сравнивая эти две величины, нужно ли мне приведение или нет?

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

Примечание: Здесь подразумевается, что и ==, и === проверяют типы своих операндов. Разница заключается в том, как они реагируют, если типы не совпадают.

Абстрактное равенство

Поведение оператора == определено в разделе 11.9.3 "Абстрактный алгоритм сравнения равенства" спецификации ES5. То, что там перечислено, - это всеобъемлющий, но простой алгоритм, в котором явно указаны все возможные комбинации типов и то, как приведения (при необходимости) должны выполняться для каждой комбинации.

Предупреждение: Когда (неявное) приведение поносят как слишком сложное и ущербное, чтобы быть "полезной сильной стороной", осуждаются именно эти правила "абстрактного равенства". Как правило, говорят, что они слишком сложны и неинтуитивны для практического изучения и использования разработчиками, и что они больше склонны вызывать ошибки в программах JS, чем обеспечивать большую читаемость кода. Я считаю, что это ошибочная предпосылка - что вы, читатели, являетесь компетентными разработчиками, которые пишут (читают и понимают!) алгоритмы (он же код) весь день напролет. Итак, далее следует простое изложение "абстрактного равенства" в простых терминах. Но я умоляю вас также прочитать раздел 11.9.3 спецификации ES5. Я думаю, вы будете удивлены тем, насколько это разумно.

По сути, в первом пункте (11.9.3.1) говорится, что, если два сравниваемых значения имеют один и тот же тип, они просто и естественно сравниваются на Идентичность, как и следовало ожидать. Например, 42 равно только 42, а "abc" равно только "abc".

Некоторые незначительные исключения из обычных ожиданий, о которых следует знать:

  • NaN никогда не равен самому себе (см. Главу 2)
  • +0 и -0 равны друг другу (см. Главу 2)

Последнее положение пункта 11.9.3.1 описывает обычное равенство == для object значений (включая function и array). Два таких значения равны только в том случае, если они оба являются ссылками на одно и то же значение. Здесь не происходит никакого приведения.

Примечание: Алгоритм сравнения для строгого равенства === определен идентично пункту 11.9.3.1, включая положение о двух значениях object. Это совсем малоизвестный факт, что == и === ведут себя одинаково в случае, когда сравниваются два object!

Остальная часть алгоритма в 11.9.3 говорит, что, если вы используете обычное равенство == для сравнения двух значений разных типов, одно или оба значения должны быть неявно приведены. Это приведение выполняется таким образом, что оба значения в конечном итоге оказываются одного типа, и могут быть напрямую сопоставлены на Идентичность значений.

Примечание: Операция != обычного неравенства определяется точно так, как вы ожидаете, в том смысле, что это буквально выполнение операция ==, а затем отрицание (инверсия) результата. То же самое относится и к строгой операции неравенства !==.

Сравнение: string и number

Чтобы проиллюстрировать приведение ==, давайте сначала рассмотрим примеры string и number, приведенные ранее в этой главе:

var a = 42;
var b = "42";

a === b;	// false
a == b;		// true

Как и следовало ожидать, a === b терпит неудачу, потому что никакое приведение не допускается, и действительно, значения 42 и "42" различные.

Однако второе сравнение a == b использует обычное равенство, что означает, что если типы окажутся разными, то алгоритм сравнения выполнит неявное приведение одного или обоих значений.

Но какой вид приведения здесь выполняется? Приводится ли значение a, равное 42, к string, или значение b, равное "42", становится number?

В спецификации ES5 в пунктах 11.9.3.4-5 говорится:

  1. Если Type(x) -- Number, а Type(y) -- String, вернуть результат сравнения x == ToNumber(y).
  2. Если Type(x) -- String, а Type(y) -- Number, вернуть результат сравнения ToNumber(x) == y.

Предупреждение: Спецификация использует Number и String в качестве формальных имен для типов, а эта книга предпочитает number и string для обозначения примитивных типов. Не позволяйте заглавным буквам Number в спецификации ввести вас в заблуждение и принять это за функцию Number(). Для наших целей заглавные буквы в названии типа не имеют значения - в целом они имеют одно и то же значение.

Очевидно, что спецификация говорит, что для сравнения значение "42" приведено к number. Как приведено - ранее было рассмотрено - с помощью абстрактной операции ToNumber. В этом случае совершенно очевидно, что результирующие два значения 42 равны.

Сравнение: Любого с boolean

Одна из самых больших ошибок с неявным приведением в обычном равенстве == возникает, когда вы пытаетесь напрямую сравнить значение с true или false.

Рассмотрим:

var a = "42";
var b = true;

a == b;	// false

Подождите, что здесь произошло!? Мы знаем, что "42" - это истинное значение (см. выше в этой главе). Итак, как вышло что в == оно не равно true?

Причина одновременно проста и обманчиво хитра. Это так легко неправильно понять, что многие разработчики JS никогда не уделяют достаточно пристального внимания, чтобы полностью понять это.

Давайте еще раз процитируем спецификацию, пункты 11.9.3.6-7:

  1. Если Type(x) -- Boolean, вернуть результат сравнения ToNumber(x) == y.
  2. Если Type(y) -- Boolean, вернуть результат сравнения x == ToNumber(y).

Давайте разберемся с этим. Во-первых:

var x = true;
var y = "42";

x == y; // false

Type(x) действительно Boolean, поэтому выполняется ToNumber(x), которое приводит true в 1. Следом анализируется 1 == "42". Типы по-прежнему разные, поэтому (по сути, рекурсивно) мы прибегаем к алгоритму, который, как и выше, преобразует "42" в 42, а 1 == 42 явно является false.

Поменяем направление на противоположное, и мы все равно получим тот же результат:

var x = "42";
var y = false;

x == y; // false

На этот раз Type(y) является Boolean, поэтому ToNumber(y) дает 0, а "42" == 0 рекурсивно становится 42 == 0, что, конечно же, является false.

Другими словами, значение "42" не является ни == true, ни == false. На первый взгляд это утверждение может показаться безумным. Как значение может быть ни правдивым, ни ложным?

Но в этом-то и проблема! Вы задаете совершенно неправильный вопрос. На самом деле это не твоя вина. Твой мозг обманывает тебя.

"42" действительно правдивое, но "42" == true вообще не выполняет логический тест/приведение, независимо от того, что говорит ваш мозг. "42" не приводится к boolean (true), а вместо этого true преобразуется в 1, а затем "42" конвертируется в 42.

Нравится нам это или нет, ToBoolean здесь даже не задействован, поэтому правдивость или ложность "42" не имеет отношения к операции ==!

Единственно, что важно, - это понять, как алгоритм == сравнения ведет себя с разными комбинациями типов. Поскольку это относится и к boolean по обе стороны от ==, запомните - boolean всегда сначала приводится к number.

Если вам это кажется странным, вы не одиноки. Лично я рекомендовал бы ни при каких обстоятельствах не использовать == true или == false. Никогда.

Но помните, я говорю только о ==. === true и === false не делают приведения типа, поэтому они защищены от этого скрытого приведения ToNumber.

Рассмотрим:

var a = "42";

// плохо (не пройдет проверку!):
if (a == true) {
	// ..
}

// тоже плохо (не пройдет проверку!):
if (a === true) {
	// ..
}

// приемлемо (неявное приведение):
if (a) {
	// ..
}

// лучше (явное приведение):
if (!!a) {
	// ..
}

// тоже хорошо (явное приведение):
if (Boolean( a )) {
	// ..
}

Если вы будете избегать в своем коде == true или == false (обычное равенство с boolean), вам никогда не придется беспокоиться об этой ментальной ловушки правдивости/ложности.

Сравнение: null с undefined

Другой пример неявного приведения можно наблюдать в случае обычного равенства == между значениями null и undefined. Еще раз цитирую спецификацию ES5, пункты 11.9.3.2-3:

  1. Если x равно null, а y -- undefined, вернуть значение true.
  2. Если x равно undefined, а y -- null, вернуть значение true.

null и undefined при обычном равенстве == приравниваются (приводятся) друг к другу (а также к самим себе, очевидно) и к никаким другим значениям языка.

Это означает, что null и undefined могут рассматриваться как неразличимые для целей сравнения, если вы используете оператор обычного равенства == для их взаимного неявного приведения.

var a = null;
var b;

a == b;		// true
a == null;	// true
b == null;	// true

a == false;	// false
b == false;	// false
a == "";	// false
b == "";	// false
a == 0;		// false
b == 0;		// false

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

Например:

var a = doSomething();

if (a == null) {
	// ..
}

Проверка a == null пройдет только в том случае, если doSomething() возвращает либо null, либо undefined, и завершится ошибкой с любым другим значением, даже с другими ложными значениями, такими как 0, false и "".

Явная форма проверки, запрещающее любое такое приведение, (на мой взгляд) неоправданно сильно уродливее (и, возможно, чуть менее производительна!):

var a = doSomething();

if (a === undefined || a === null) {
	// ..
}

По-моему, форма a == null - это еще один пример, когда неявное приведение улучшает читаемость кода, и делает это безопасным способом.

Сравнение: object с не-object

Пункты 11.9.3.8-9 спецификация ES5 гласят, что, если object/function/array сравнивается с простым скалярным примитивом (string, number или boolean), то:

  1. Если Type(x) является String или Number, а Type(y) - Object, вернуть результат сравнения x == ToPrimitive(y).
  2. Если Type(x) - Object, а Type(y) - String или Number, вернуть результат сравнения ToPrimitive(x) == y.

Примечание: Вы можете заметить, что в этих положениях упоминаются только String и Number, но не Boolean. Это потому, что пункты 11.9.3.6-7 (см. выше) позаботятся о том, чтобы сначала привести Boolean операнд к Number.

Рассмотрим:

var a = 42;
var b = [ 42 ];

a == b;	// true

Для значения [ 42 ] вызывается абстрактная операция ToPrimitive (см. выше раздел "Абстрактные операции со значениями"), результатом которой станет "42". Теперь это просто 42 == "42", которое, как мы уже видели, становится 42 == 42, так что a и b оказываются приведённо равными.

Совет: Все особенности абстрактной операции ToPrimitive, которые мы обсуждали ранее в этой главе (toString(), valueOf()), применимы здесь, как и следовало ожидать. Это может быть весьма полезно, если у вас сложная структура данных, для которой вы хотите определить пользовательский метод valueOf(), чтобы предоставить простое значение для сравнения на равенство.

В Главе 3 мы рассматривали "распаковку", когда object-обёртка вокруг примитивного значения (например, из new String("abc")) разворачивается, и возвращается базовое примитивное значение ("abc"). Такое поведение связано с ToPrimitive-приведением в алгоритме ==:

var a = "abc";
var b = Object( a );	// same as `new String( a )`

a === b;				// false
a == b;					// true

a == b является true, потому что b принудительно (он же "unboxed", развернутый) через ToPrimitive к его простому скалярному примитивному значению abc, которое совпадает со значением в a.

Однако есть некоторые значения, для которых это не так, из-за других переопределяющих правил в алгоритме ==. Посмотрите:

var a = null;
var b = Object( a );	// same as `Object()`
a == b;					// false

var c = undefined;
var d = Object( c );	// same as `Object()`
c == d;					// false

var e = NaN;
var f = Object( e );	// same as `new Number( e )`
e == f;					// false

Значения null и undefined не могут быть "упакованы" - у них нет эквивалента объектной обёртки - поэтому Object(null) аналогичен Object() в том смысле, что оба они просто создают обычный объект.

NaN может быть упакован в эквивалент объектной оболочки Number, но когда == вызывает распаковку, сравнение NaN == NaN завершается неудачей, потому что NaN никогда не равен самому себе (см. Главу 2).

Граничные случаи

Теперь, когда мы тщательно изучили, как работает неявное приведение обычного равенства == (как логичным, так и неожиданным образом), давайте попробуем назвать худшие, самые безумные граничные случаи, чтобы мы понять, чего нам надо избегать, чтобы не быть нокаутированным ошибками приведения.

Во-первых, давайте рассмотрим, как модификация встроенных прототипов может привести к сумасшедшим результатам:

Number из любого другого значения будет...

Number.prototype.valueOf = function() {
	return 3;
};

new Number( 2 ) == 3;	// true

Предупреждение: 2 == 3 не попало бы в эту ловушку, потому что ни 2, ни 3 не вызвали бы встроенный метод Number.prototype.valueOf(), потому что оба уже являются примитивными значениями number и могут быть сравнены напрямую. Однако new Number(2) должно пройти через приведение ToPrimitive и, таким образом, вызвать valueOf().

Зло, да? Конечно. Никто никогда не должен делать ничего подобного. Тот факт, что вы можете сделать это, иногда используется как критика приведения и ==. Но это беспочвенное разочарование. JavaScript не плох, потому что вы можете делать такие вещи, разработчик плох, если он делает такие вещи. Не впадайте в заблуждение "мой язык программирования должен защищать меня от самого себя".

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

if (a == 2 && a == 3) {
	// ..
}

Вы могли подумать, что это невозможно, потому что a никогда не может быть равно как 2, так и 3 одновременно. Но "в то же время" неточно, поскольку первое выражение a == 2 происходит строго перед a == 3.

Итак, что, если мы заставим a.valueOf() иметь побочные эффекты при каждом его вызове, так что при первом вызове он вернет 2, а при втором - 3? Это довольно просто:

var i = 2;

Number.prototype.valueOf = function() {
	return i++;
};

var a = new Number( 42 );

if (a == 2 && a == 3) {
	console.log( "Да, это случилось." );
}

Опять же, это злые уловки. Не делайте так. И не используйте их для жалоб на приведение. Потенциальные злоупотребления механизмом не являются достаточными доказательствами для осуждения механизма. Просто избегайте этих безумных трюков и придерживайтесь только корректного и надлежащего использования приведения.

False-сравнения

Наиболее частая жалоба на неявное приведение при обычном равенстве == исходит из того, что ложные значения ведут себя неожиданно, когда сравниваются друг с другом.

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

"0" == null;			// false
"0" == undefined;		// false
"0" == false;			// true -- О-О-О!
"0" == NaN;				// false
"0" == 0;				// true
"0" == "";				// false

false == null;			// false
false == undefined;		// false
false == NaN;			// false
false == 0;				// true -- О-О-О!
false == "";			// true -- О-О-О!
false == [];			// true -- О-О-О!
false == {};			// false

"" == null;				// false
"" == undefined;		// false
"" == NaN;				// false
"" == 0;				// true -- О-О-О!
"" == [];				// true -- О-О-О!
"" == {};				// false

0 == null;				// false
0 == undefined;			// false
0 == NaN;				// false
0 == [];				// true -- О-О-О!
0 == {};				// false

В этом списке 17 из 24 сравнений вполне разумны и предсказуемы. Например, мы знаем, что "" и NaN вовсе не являются приравниваемыми значениями, и действительно, они не приводятся обычным равенством, тогда как "0" и 0 разумно приравниваемы и действительно приводятся обычным равенством.

Однако семь сравнений отмечены знаком "О-О-О!", потому что, будучи ложнопозитивными, они скорее являются ошибками, которые могут сбить с толку. "" и 0 совершенно разные значения, и редко, когда есть необходимость считать их равнозначными. Поэтому их взаимное приведение вызывает проблемы. Обратите внимание, что здесь нет никаких ложнонегативных случаев.

Самые сумасшедшие из них

Однако мы не должны останавливаться на достигнутом. Мы можем продолжать искать еще более неприятные способы приведения:

[] == ![];		// true

Оооо, это кажется более высоким уровнем сумасшествия, верно!? Ваш мозг, скорее всего, может обмануть вас, что вы сравниваете правдивое и ложное значения, поэтому результат true удивителен, поскольку мы знаем, что значение никогда не может быть правдивым и ложным одновременно!

Но это не то, что происходит на самом деле. Давайте разберем это по полочкам. Что мы знаем об унарном операторе !? Он явно приводит к boolean, используя правила ToBoolean (он также инвертирует значение). Таким образом, еще до того, как [] == ![] будет обработано, оно фактически уже переведено в [] == false. Мы уже видели эту в нашем списке выше (false == []), поэтому неожиданный результат не новый для нас.

Как насчет других граничных случаев?

2 == [2];		// true
"" == [null];	// true

Как мы говорили ранее в нашем обсуждении ToNumber, значения справа [2] и [null] подвергнутся приведению ToPrimitive, поэтому их будет легче сравнить с простыми примитивами (2 и "" соответственно) слева. Поскольку valueOf() для значений array просто возвращает сам array, приведение сводится к превращению array в строку.

[2] станет "2", которая затем приводится ToNumber к 2 справа в первом сравнении. [null] просто прямо становится "".

Поэтому 2 == 2 и "" == "" вполне понятны.

Если вам по-прежнему инстинктивно не нравятся эти результаты, то на самом деле ваше разочарование вызвано не приведением, как вы вероятно думаете. На самом деле это недовольство значением по умолчанию для array, когда приведение ToPrimitive возвращает значение string. Скорее всего, вы бы просто хотели, чтобы [2].toString() не возвращал "2", и [null].toString() возвращал бы не "".

Но что должны возвращать эти приведения к string? Я действительно не могу придумать ничего более подходящего для [2] приводимой к string, кроме "2", за исключением, возможно, "[2]" - но это может оказаться очень странным в других контекстах!

Вы могли бы справедливо утверждать, что поскольку String(null) становится "null", то String([null]) также должна стать "null". Это разумное утверждение. Итак, вот кто настоящий виновник.

Неявное приведение само по себе здесь не является злом. Даже явное приведение [null] к string приводит к "". Что вызывает разногласия, так это, разумно ли вообще array приводить к строковому эквиваленту его содержимого, и как именно это делать. Итак, направьте свое разочарование на правила для String( [..] ), потому что именно отсюда рождается сумасшествие. Возможно, вообще не должно быть приведения array к строке? Но это дало бы много других нехороших последствий в других частях языка.

Еще одно часто цитируемое недоумение:

0 == "\n";		// true

Как мы обсуждали выше пустые "", "\n" (" " или любая другая комбинация пробелов) приводится через ToNumber, и результат этого - 0. К какому другому значению number вы хотели привести пробел? Вас беспокоит, что явное Number(" ") дает 0?

На самом деле единственное другое разумное значение number, к которому можно привести пустые строки или строки с пробелами, - это NaN. Но действительно ли это было бы лучше? Сравнение " " == NaN, конечно же, потерпело бы неудачу, но неясно, действительно ли бы мы исправили какие-либо из проблем.

Вероятность того, что реальная JS-программа выйдет из строя из-за 0 == "\n", ужасно редка, и таких граничных случаев легко избежать.

Преобразования типов всегда имеют граничные случаи на любом языке - ничего специфичного для приведения. Проблемы здесь заключаются в последующих сомнениях о конкретных граничных случаях (и, возможно, это правильно!?), но это не является весомым аргументом против общего механизма приведения.

Итог: почти любое сумасшедшее приведение между нормальными значениями, с которым вы вероятно столкнетесь (кроме намеренных хаков valueOf() и toString() выше), будет сводиться к короткому списку из семи пунктов, которые мы определили выше.

В противовес этим 24-м частым подозреваемым в странном приведении, рассмотрим другой список:

42 == "43";							// false
"foo" == 42;						// false
"true" == true;						// false

42 == "42";							// true
"foo" == [ "foo" ];					// true

В этих не ложных, не граничных случаях (а существует буквально бесконечное число сравнений, которые мы могли бы включить в этот список) результаты приведения абсолютно безопасны, разумны и объяснимы.

Проверка на здравомыслие

Ладно, мы действительно нашли какие-то сумасшедшие вещи, когда глубоко погрузились в неявное приведение. Неудивительно, что большинство разработчиков заявляют, что приведение - это зло, и его следует избегать, верно!?

Но давайте сделаем шаг назад и проведем проверку на здравомыслие.

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

Если вы ищете хрестоматийный пример "выплёскивания вместе с водой ребёнка", то вот он: отказ от любого приведения (бесконечно большой список безопасных и полезных средств) из-за буквально семи ошибок.

Более разумной реакцией было бы спросить: "Как я могу использовать бесчисленные сильные стороны приведения, и избежать немногих недостатков?"

Давайте еще раз посмотрим на список "недостатков":

"0" == false;			// true -- О-О-О!
false == 0;				// true -- О-О-О!
false == "";			// true -- О-О-О!
false == [];			// true -- О-О-О!
"" == 0;				// true -- О-О-О!
"" == [];				// true -- О-О-О!
0 == [];				// true -- О-О-О!

Четыре из семи пунктов в этом списке включают сравнение == false, которого, как мы говорили ранее, вам следует всегда, всегда избегать. Это довольно простое правило для запоминания.

Теперь список сократился до трех.

"" == 0;				// true -- О-О-О!
"" == [];				// true -- О-О-О!
0 == [];				// true -- О-О-О!

Будет ли применение таких приведений в обычной JS-программе разумным? В каких обстоятельствах они действительно могут случиться?

Я не думаю, что высока вероятность того, что вы буквально будете использовать == [] в вашей программе. По крайней мере, намеренно. Более вероятно, вы бы написали == "" или == 0, как тут:

function doSomething(a) {
	if (a == "") {
		// ..
	}
}

У вас бы случился большой ОЙ, если бы вы случайно вызвали doSomething(0) или do Something([]). Другой сценарий:

function doSomething(a,b) {
	if (a == b) {
		// ..
	}
}

Опять же, это может сломаться, если вы сделаете что-то вроде doSomething("",0) или doSomething([],"").

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

Безопасное использование неявного приведения

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

  1. Если любая из сторон сравнения может иметь значения true или false, никогда, НИКОГДА не используйте ==.
  2. Если любая из сторон сравнения может иметь [], "", или значения 0, серьезно подумайте о том, чтобы не использовать ==.

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

Для большей ясности - это избавит вас от множества головных проблем.

Вопрос == vs. === в действительности формулируется так: следует ли допускать приведение для сравнения или нет?

Есть много случаев, когда такое приведение может быть полезным, позволяя вам более кратко выразить некоторую логику сравнения (например, с null и undefined).

В совокупном порядке вещей существует относительно немного случаев, когда неявное приведение действительно опасно. Но в этих ситуациях, в целях безопасности, обязательно используйте ===.

Совет: Еще один случай, где приведение гарантированно не причинит вам вреда, - это оператор typeof. typeof всегда будет возвращать вам одну из семи строк (см. Главу 1), и ни одна из них не является пустой строкой "". Соответственно, нет ни одного случая, когда проверка типа какого-либо значения натолкнется на неявное приведение. typeof x == "function" на 100% так же безопасен и надежен, как typeof x === "function". В спецификации буквально говорится, что алгоритм будет идентичен в этих случаях. Итак, не используйте === слепо везде просто потому, что так говорит ваша среда разработки, или (что хуже всего) потому, что в какой-то книге вам сказали не думать об этом. Вы сами отвечаете за качество своего кода.

Является ли неявное приведение злом и опасным? В некоторых случаях - да, но в подавляющем большинстве - нет.

Будьте ответственным и зрелым разработчиком. Изучите, как эффективно и безопасно применять мощь приведения (как явного, так и неявного). И научите окружающих делать то же самое.

Вот удобная таблица, составленная Алексом Дори (@dorey на GitHub) для визуализации различных сравнений:

Источник: https://github.com/dorey/JavaScript-Equality-Table

Абстрактное сравнение отношений

Хотя этой части неявного приведения часто уделяется гораздо меньше внимания, тем не менее важно подумать о том, что происходит со сравнениями a < b (аналогично тому, как мы только что подробно рассмотрели a == b).

Алгоритм "Абстрактного сравнения отношений" в разделе 11.8.5 ES5 по существу делится на две части: что делать, если в сравнении оба значения string (вторая половина) или иное (первая половина).

Примечание: Алгоритм определен только для a < b. Поэтому a > b обрабатывается как b < a.

Сначала алгоритм инициирует приведение ToPrimitive для обоих значений, и если любой из возвращаемых результатов не string, то оба значения преобразуются в number с использованием правил операции ToNumber и затем сравниваются численно.

Например:

var a = [ 42 ];
var b = [ "43" ];

a < b;	// true
b < a;	// false

Примечание: Здесь действуют те же оговорки о -0 и NaN, как и в рассмотренном ранее алгоритме ==.

Однако, если в сравнении < оба значения string, выполняется простое лексикографическое (в натуральном алфавите) сравнение символов:

var a = [ "42" ];
var b = [ "043" ];

a < b;	// false

a и b не приводятся к number, потому что оба array после ToPrimitive приведения становятся string. Следовательно, "42" посимвольно сравнивается с "043", начиная с первых символов "4" и "0" соответственно. Поскольку "0" лексикографически меньше, чем "4", сравнение возвращает false.

Точно такое же поведение и рассуждения наблюдаем здесь:

var a = [ 4, 2 ];
var b = [ 0, 4, 3 ];

a < b;	// false

Здесь a становится "4,2", а b становится "0,4,3", и они лексикографически сравниваются идентично предыдущему коду.

Как насчет этого:

var a = { b: 42 };
var b = { b: 43 };

a < b;	// ??

a < b также false, потому что a становится [object Object], и b становится [object Object]. Поэтому очевидно, что a лексикографически не меньше b.

Но вот странный случай:

var a = { b: 42 };
var b = { b: 43 };

a < b;	// false
a == b;	// false
a > b;	// false

a <= b;	// true
a >= b;	// true

Почему a == b не true? Это же одно и то же string значение ("[object Object]"), поэтому кажется они должны быть равны, верно? Нет. Вспомним предыдущее обсуждение того, как == работает со ссылками на object.

Но тогда как a <= b и a >= b становятся true, если и a < b, и a == b, и a > b - все равны false?

Потому что в спецификации сказано, что для a <= b, на самом деле сначала вычисляется b < a, а затем её результат инвертируется. Так как b < a тоже false, то результат a <= b это true.

Это, вероятно, ужасно противоречит тому, как до настоящего момента вы вероятно интерпретировали действие <= - скорее буквально: "меньше, чем или равно". JS интерпретирует <= как "не больше, чем" (!(a > b), которое JS трактует как !(b < a)). Более того, a >= b также сначала интерпретируется как b <= a, а затем в ход идёт та же логика.

К сожалению, не существует "строгого сравнения отношения", как для равенства. Другими словами, нет никакого способа предотвратить неявное приведение при сравнении отношений, таких как a < b, кроме как явно убедиться, что a и b имеют один и тот же тип, перед выполнением сравнения.

Следуйте той же логике проверки на здравомыслие, что и в нашем предыдущем обсуждении == vs. ===. Если приведение полезно и достаточно безопасно, как в сравнении 42 < "43", используйте его. С другой стороны, если вам нужна безопасность сравнении отношений, то сначала явно приведите значения, прежде чем использовать < (или его аналоги).

var a = [ 42 ];
var b = "043";

a < b;						// false -- сравнение строк!
Number( a ) < Number( b );	// true -- сравнение чисел!

Итоги

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

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

Явное приведение - это код, из которого очевидно, что целью является преобразование значения из одного типа в другой. Его преимущество - улучшение читаемости и поддержки кода путем сокращения недоумений.

Неявное приведение - это "скрытое" преобразование типа, являющееся побочным эффектом какой-либо другой операции, где не так очевидно, что произойдет конвертация типа. Хотя может показаться, что неявное приведение противопоставляется явному и, поэтому вредно (и действительно, многие так думают!), на самом деле неявное приведение также служит улучшению читаемости кода.

Приведение должно использоваться ответственно и осознано, особенно это касается неявного приведения. Знайте, почему вы пишете тот код, который вы пишете, и как он работает. Стремитесь писать код, который другие также легко смогут изучить и понять.