Обобщим здесь информацию о системе преобразований типов в JavaScript.
Она очень проста, но отличается от других языков. Поэтому она часто служит «камнем преткновения» для приходящих в JavaScript программистов.
Всего есть три преобразования:
- Cтроковое преобразование.
- Числовое преобразование.
- Преобразование к логическому значению.
Примитивы
Сначала рассмотрим преобразования, которые происходят с обычными значениями, т.е. когда в операции не участвуют объекты.
Строковое преобразование
Строковое преобразование происходит, когда требуется представление чего-либо в виде строки. Например, его производит функция alert.
var a = true; alert(a); // "true"
Можно также осуществить преобразование явным вызовом String(val):
alert( String(null) === "null" ); // true
На практике для явного преобразования часто применяется оператор "+", у которого один из аргументов строка. В этом случае он приводит к строке и другой аргумент:
alert( true + "test" ); // "truetest" alert( "123" + undefined); // "123undefined"
Численное преобразование
Численное преобразование происходит в математических функциях и выражениях, а также при нестрогом сравнении данных различных типов.
Для преобразования к числу в явном виде можно вызвать Number(val), либо, что короче, поставить перед выражением оператор "+":
var a = +"\n123\n"; // 123
var a = Number("\n123\n"); // 123, тот же эффект
| Значение | Преобразуется в… |
|---|---|
undefined |
NaN |
null |
0 |
true/false |
1/0 |
| Строка | Пробелы по краям обрезаются. Далее, если остаётся пустая строка, то 0. Из непустой «считывается» число. Если это хоть немного не число, то результат NaN. |
Примеры:
- Логические значения:
alert( +true ); // 1 alert( +false); // 0
- Сравнение разных типов — значит численное преобразование:
alert( "\n0\n" == 0 ); // true
При этом строка
"\n0\n"преобразуется к числу — начальные и конечные пробелы игнорируются, получается0. -
Еще пример:
alert( "\n" == false );
Здесь сравнение
==снова приводит обе части к числу. И слева и справа получается0.По аналогичной причине верно равенство
"1" == true.
Специальные значения
Посмотрим на поведение специальных значений более внимательно.
Интуитивно, значения null/undefined ассоциируются с нулём, но при преобразованиях ведут себя иначе.
Специальные значения преобразуются к числу так:
| Значение | Преобразуется в… |
|---|---|
undefined |
NaN |
null |
0 |
Это преобразование осуществляется при арифметических операциях и сравнениях > >= < <=, но не при проверке равенства ==. Алгоритм проверки равенства для этих значений в спецификации прописан отдельно (пункт 11.9.3). В нём считается, что null и undefined равны "==" между собой, но эти значения не равны никакому другому значению.
Это ведёт к забавным последствиям.
Например, null не подчиняется законам математики — он больше либо равен >=, но не больше и не равен:
alert(null >= 0); // true, т.к. null преобразуется к 0 alert(null > 0); // false (не больше), т.к. null преобразуется к 0 alert(null == 0 ); // false (и не равен!), т.к. == рассматривает null особо.
Значение undefined вообще вне сравнений:
alert(undefined > 0); // false, т.к. undefined -> NaN alert(undefined == 0); // false, т.к. это undefined (без преобразования) alert(undefined < 0); // false, т.к. undefined -> NaN
По возможности, лучше не давать специальным значениям участвовать в сравнениях > >= < <=. Используйте для таких переменных числа или приводите к числу явно, это сделает код более прозрачным и надежным.
Логическое преобразование
Преобразование к true/false происходит в логическом контексте, таком как if(obj), while(obj) и при применении логических операторов.
Все значения, которые интуитивно близки к «ничто», становятся false. Например: 0, пустая строка, null, undefined, NaN. Остальные, в том числе и любые объекты — true.
Полная таблица преобразований:
| Значение | Преобразуется в… |
|---|---|
undefined, null |
false |
| Числа | Все true, кроме 0, NaN — false. |
| Строки | Все true, кроме пустой строки "" — false |
| Объекты | Всегда true |
"0" становится trueВ отличие от многих языков программирования (например PHP), "0" в JavaScript является true, как и строка из пробелов:
if ("0" && " ") {
alert("любые непустые строки - true!");
}
Логическое преобразование интересно тем, как оно сочетается с численным.
Два значения могут быть равны, но одно из них в логическом контексте true, другой — false.
Например, равенства в следующем примере верны, так как происходит численное преобразование:
alert( 0 == "\n0\n" ); // true alert( false == " " ); // true
.. А в логическом контексте левая часть даст false, правая — true:
if ("\n0\n") {
alert("true, совсем не как 0!");
}
С точки зрения преобразования типов в JavaScript это совершенно нормально. При равенстве — численное преобразование, а в if — логическое, только и всего.
Объекты
В объектах механизм строкового и численного преобразования можно переопределить. Посмотрим, как это делается.
Строковое преобразование
Начнем с небольшого примера. Создадим и выведем объект:
var obj = { firstName: 'Василий' };
alert(obj); // [object Object]
Как видно, содержимое объекта не вывелось. Это потому, что стандартным строковым представлением пользовательского объекта является строка "[object Object]".
Такой вывод объекта не содержит интересной информации. Поэтому имеет смысл его поменять на что-то более полезное.
Если в объекте присутствует метод toString, который возвращает примитив, то он используется для преобразования.:
var user = {
firstName: 'Василий',
*!*toString:*/!* function() {
return 'Пользователь ' + this.firstName;
}
};
alert( user ); // Пользователь Василий
toString может иметь любой тип, кроме объектаМетод toString не обязан возвращать именно строку.
Его результат может быть любого примитивного типа. Например, это может быть число, как в примере ниже:
var obj = {
toString: function() { return 123; }
}
alert(obj); // 123
То есть, строковое преобразование должно возвращать любой примитив, а вовсе не обязательно — строку.
Поэтому мы и называем его здесь «строковое преобразование», а не «преобразование к строке».
Все объекты, включая встроенные, имеют свои реализации метода toString, например:
alert( [1,2] ); // toString для массивов выводит список элементов "1,2"
alert( new Date ); // toString для дат выводит дату в виде строки
alert( function() { } ); // toString для функции выводит её код
Численное преобразование
Для численного преобразования объекта используется метод valueOf (возвращающий примитив), а если его нет — то toString:
var room = {
number: 777,
valueOf: function() { return this.number; },
toString: function() { return this.number; }
}
alert( +room ); // 777, *!*вызвался valueOf*/!*
delete room.valueOf;
alert( +room ); // 777, *!*вызвался toString*/!*
Метод valueOf обязан возвращать примитивное значение, иначе его результат будет проигнорирован. При этом — не обязательно числовое.
У большинства встроенных объектов такого valueOf нет, поэтому численное и строковое преобразования для них работают одинаково.
Исключением является объект Date, который поддерживает оба типа преобразований:
alert( new Date() ); // toString: Дата в виде читаемой строки alert( +new Date() ); // valueOf: кол-во миллисекунд, прошедших с 1.01.1970
valueOfЕсли посмотреть в стандарт, то в 15.2.4.4 определён valueOf для любых объектов. Но он ничего не делает, просто возвращает сам объект (не-примитивное значение!), а потому игнорируется.
Сравнение объектов
Два объекта равны лишь тогда, когда это один и тот же объект.
if (obj1 == obj2) {
// true, только если две переменные ссылаются на один объект
}
Если же операция требует примитива — например, это арифметика или сравнение > < >= <=, или вызов математической функции с аргументом-объектом, то объект первым делом преобразуется в примитив, а потом уже идёт операция с примитивами.
Например, сложение new Date + [] сначала преобразует дату и массив в примитивные значения, а уже потом их сложит.
Преобразование объекта в примитив
При приведении объекта к примитиву используется численное преобразование.
Например:
var obj = {
valueOf: function() { return 1; }
};
alert(obj == true); // true
alert(obj + "test"); // 1test
Пример ниже демонстрирует, что несмотря на то, что приведение «численное» — результатом может быть любое примитивное значение:
var a = {
valueOf: function() { return "1"; }
};
var b = {
valueOf: function() { return true; }
};
alert(a + b); // "1true"
Если дальнейшие операции требуют числа, то будет приведение уже не просто к примитиву, а к числу:
var a = {
valueOf: function() { return "1"; }
};
var b = {
valueOf: function() { return "2"; }
};
alert(a - b); // -1
DateСуществует исключение: объект Date преобразуется в примитив, используя строковое преобразование. С этим можно столкнуться в операторе "+":
// бинарный вариант, преобразование к примитиву alert( new Date + "" ); // "строка даты" // унарный вариант, наравне с - * / и другими приводит к числу alert( +new Date ); // число миллисекунд
В языке Java булевы значения можно создавать, используя синтаксис new Boolean(true/false). Также можно преобразовывать значения к булевому типу, применяя к ним new Boolean.
В JavaScript это не работает.
Например:
var value = new Boolean(false);
if ( value ) {
alert(true) // сработает!
}
Почему запустился alert? Ведь в if находится false… Проверим:
var value = new Boolean(false);
*!*
alert(value); // выводит false, все ок..
*/!*
if ( value ) {
alert(true); // ..но тогда почему выполняется alert в if ?!?
}
Дело в том, что new Boolean - это объект. В логическом контексте он, безусловно, true. Поэтому работает первый пример.
А второй пример вызывает alert, который преобразует объект к строке, и он становится "false".
Чтобы преобразовать значение к логическому типу, нужно использовать двойное отрицание: !!val или прямой вызов Boolean(val).
Почему результат true ?
alert( ['x'] == 'x' );
Если с одной стороны — объект, а с другой — нет, то сначала приводится объект.
В данном случае сравнение означает численное приведение. У массивов нет valueOf, поэтому вызывается toString, который возвращает список элементов через запятую.
В данном случае, элемент только один - он и возвращается. Так что ['x'] становится 'x'. Получилось 'x' == 'x', верно.
P.S.
По той же причине верны равенства:
alert( ['x','y'] == 'x,y' ); // true alert( [] == '' ); // true
Почему первое равенство — неверно, а второе — верно?
alert( [] == [] ); // false alert( [] == ![] ); // trueКакие преобразования происходят при вычислении?
Два объекта равны только тогда, когда это один и тот же объект.
В первом равенстве создаются два массива, это разные объекты, так что они неравны.
- Первым делом, обе части сравнения вычисляются. Справа находится
![]. Логическое НЕ'!'преобразует аргумент к логическому типу. Массив является объектом, так что этоtrue. Значит, правая часть становится![] = !true = false. Так что получили:
alert( [] == false );
- Проверка равенства между объектом и примитивом вызывает численное преобразование объекта.
У массива нет
valueOf, сработаетtoStringи преобразует массив в список элементов, то есть - в пустую строку:
alert( '' == false );
- Сравнение различных типов вызывает численное преобразование слева и справа:
alert( 0 == 0 );
Теперь результат очевиден.
Подумайте, какой результат будет у выражений ниже. Тут не только преобразования типов. Когда закончите - сверьтесь с решением.
'' + 1 + 0
true + false
6 / "3"
"2" * "3"
4 + 5 + "px"
"$" + 4 + 5
"4" - 2
"4px" - 2
7 / 0
({})[0]
parseInt("09")
5 && 2
2 && 5
5 || 0
0 || 5
null + 1
undefined + 1
[1] + 1
[] + null + 1
[[0]][0][0]
'' + 1 + 0 = "10"
true + false = 1
6 / "3" = 2
"2" * "3" = 6
4 + 5 + "px" = "9px"
"$" + 4 + 5
= "$45"
"4" - 2
= 2
"4px" - 2
= NaN
7 / 0
= Infinity
({})[0]
= undefined // (1)
parseInt("09")
= "0" или "9" // (2)
5 && 2
= 2
2 && 5
= 5
5 || 0
= 5
0 || 5 = 5
null + 1 = 1 // (3)
undefined + 1 = NaN // (4)
[1] + 1 = "11" // (5)
[] + null + 1 = "null1" // (6)
[[0]][0][0] = 0 // (7)
- Фигурные скобки — это создание пустого объекта, у него нет свойства
'0'. Так чтоundefined.
Обратите внимание на внешние, круглые скобки. Если их убрать и запустить{}[0]в отладочной консоли браузера — будет0, т.к. скобки{}будут восприняты как пустой блок кода, после которого идёт массив. - В некоторых браузерах parseInt без второго аргумента интерпретирует 09 как восьмиричное число.
nullпри численном преобразовании становится0undefinedпри численном преобразовании становитсяNaN- Массив преобразуется в строку
"1". Оператор"+"при сложении со строкой приводит второй аргумент к строке — значит будет"1" + "1" = "11". - Массив преобразуется в пустую строку
"" + null + 1, оператор"+"видит, что слева строка и преобразуетnullк строке, получается"null" + 1, и в итоге"null1". [[0]]— это вложенный массив[0]внутри внешнего[ ]. Затем мы берём от него нулевой элемент, и потом еще раз.Если это непонятно, то посмотрите на такой пример:
alert( [1,[0],2][1] );
Квадратные скобки после массива/объекта обозначают не другой массив, а взятие элемента.
Рецепты
Обобщим рецепты для частых случаев жизни:
- Преобразование к числу
+value- Преобразование к строке объекта без
valueOf value + ''- Преобразование к логическому значению
!!value
Итого
В JavaScript есть три преобразования, которые зависят от контекста:
- Строковое: происходит обычно при выводе объекта, использует
toString. - Численное: его делают математические операторы и функции, а также сравнения и проверки равенства, кроме строгих
=== и !==. ИспользуетvalueOf, если есть, а если нет —toString. - Логическое: происходит по таблице.
Преобразовать значение явным образом можно так:
- К строке:
'' + value. - К числу:
+value. - К логическому:
!!value.
Преобразование типов не осуществляется в следующих случаях:
- При сравнении объектов. Объекты равны только когда это один и тот же объект.
- При сравнении двух строк. Там отдельный алгоритм сравнения. А вот если хоть один тип — не строка, то значения будут приведены:
true > "000"станет1 > 0. - При проверке равенства с
nullиundefined(они равны друг другу, но не равны чему бы то ни было ещё), этот случай прописан особо в спецификации.
Полный алгоритм преобразований есть в спецификации EcmaScript, смотрите пункты 11.8.5, 11.9.3, а также 9.1 и 9.3.
Комментарии
- Приветствуются комментарии, содержащие дополнения и вопросы по статье, и ответы на них.
- Если ваш комментарий касается задачи -- откройте её в отдельном окне и напишите там.
- Комментарии без смысла, с рекламой или не о статье вообще - удаляются.