Мастер-классы по Javascript Екатеринбург Ростов-на-Дону Москва Узнать больше...
Содержание (скрыть) Содержание (показать)

Преобразование типов, toString и valueOf

  1. Примитивы
    1. Строковое преобразование
    2. Численное преобразование
    3. Специальные значения
    4. Логическое преобразование
  2. Объекты
    1. Строковое преобразование
    2. Численное преобразование
    3. Сравнение объектов
    4. Преобразование объекта в примитив
  3. Рецепты
  4. Итого

Обобщим здесь информацию о системе преобразований типов в JavaScript.

Она очень проста, но отличается от других языков. Поэтому она часто служит «камнем преткновения» для приходящих в JavaScript программистов.

Всего есть три преобразования:

  1. Cтроковое преобразование.
  2. Числовое преобразование.
  3. Преобразование к логическому значению.

Примитивы

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

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

Строковое преобразование происходит, когда требуется представление чего-либо в виде строки. Например, его производит функция 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, NaNfalse.
Строки Все 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-разработчика

В языке 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
Какие преобразования происходят при вычислении?

Ответ по первому равенству
Решение
Ответ по первому равенству

Два объекта равны только тогда, когда это один и тот же объект.

В первом равенстве создаются два массива, это разные объекты, так что они неравны.

Ответ по второму равенству
Ответ по второму равенству
  1. Первым делом, обе части сравнения вычисляются. Справа находится ![]. Логическое НЕ '!' преобразует аргумент к логическому типу. Массив является объектом, так что это true. Значит, правая часть становится ![] = !true = false. Так что получили:
    alert( [] == false );
    
  2. Проверка равенства между объектом и примитивом вызывает численное преобразование объекта.

    У массива нет valueOf, сработает toString и преобразует массив в список элементов, то есть - в пустую строку:

    alert( '' == false );
    

  3. Сравнение различных типов вызывает численное преобразование слева и справа:
    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)

  1. Фигурные скобки — это создание пустого объекта, у него нет свойства '0'. Так что undefined.
    Обратите внимание на внешние, круглые скобки. Если их убрать и запустить {}[0] в отладочной консоли браузера — будет 0, т.к. скобки {} будут восприняты как пустой блок кода, после которого идёт массив.
  2. В некоторых браузерах parseInt без второго аргумента интерпретирует 09 как восьмиричное число.
  3. null при численном преобразовании становится 0
  4. undefined при численном преобразовании становится NaN
  5. Массив преобразуется в строку "1". Оператор "+" при сложении со строкой приводит второй аргумент к строке — значит будет "1" + "1" = "11".
  6. Массив преобразуется в пустую строку "" + null + 1, оператор "+" видит, что слева строка и преобразует null к строке, получается "null" + 1, и в итоге "null1".
  7. [[0]] — это вложенный массив [0] внутри внешнего [ ]. Затем мы берём от него нулевой элемент, и потом еще раз.

    Если это непонятно, то посмотрите на такой пример:

    alert( [1,[0],2][1] );
    

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

Рецепты

Обобщим рецепты для частых случаев жизни:

Преобразование к числу
+value
Преобразование к строке объекта без valueOf
value + ''
Преобразование к логическому значению
!!value

Итого

В JavaScript есть три преобразования, которые зависят от контекста:

  1. Строковое: происходит обычно при выводе объекта, использует toString.
  2. Численное: его делают математические операторы и функции, а также сравнения и проверки равенства, кроме строгих === и !==. Использует valueOf, если есть, а если нет — toString.
  3. Логическое: происходит по таблице.

Преобразовать значение явным образом можно так:

  • К строке: '' + value.
  • К числу: +value.
  • К логическому: !!value.

Преобразование типов не осуществляется в следующих случаях:

  • При сравнении объектов. Объекты равны только когда это один и тот же объект.
  • При сравнении двух строк. Там отдельный алгоритм сравнения. А вот если хоть один тип — не строка, то значения будут приведены: true > "000" станет 1 > 0.
  • При проверке равенства с null и undefined (они равны друг другу, но не равны чему бы то ни было ещё), этот случай прописан особо в спецификации.

Полный алгоритм преобразований есть в спецификации EcmaScript, смотрите пункты 11.8.5, 11.9.3, а также 9.1 и 9.3.


Комментарии

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

Содержание

Реклама

Нашли опечатку?

Нашли опечатку на сайте? Что-то кажется странным?
Выделите соответствующий текст и нажмите Ctrl+Enter!

Последние Комментарии

Помоги другим!

Помоги другим узнать о хорошей статье!