en.javascript.info/1-js/6-objects-more/2-object-conversion/article.md
2015-05-22 21:07:46 +03:00

281 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Преобразование объектов: toString и valueOf
Ранее, в главе [](/types-conversion) мы рассматривали преобразование типов для примитивов. Теперь добавим в нашу картину мира объекты.
Бывают операции, при которых объект должен быть преобразован в примитив.
[cut]
Например:
<ul>
<li>Строковое преобразование -- если объект выводится через `alert(obj)`.</li>
<li>Численное преобразование -- при арифметических операциях, сравнении с примитивом.</li>
<li>Логическое преобразование -- при `if(obj)` и других логических операциях.</li>
</ul>
Рассмотрим эти преобразования по очереди.
## Логическое преобразование
Проще всего -- с логическим преобразованием.
**Любой объект в логическом контексте -- `true`, даже если это пустой массив `[]` или объект `{}`.**
```js
//+ run
if ({} && []) {
alert( "Все объекты - true!" ); // alert сработает
}
```
## Строковое преобразование
Строковое преобразование проще всего увидеть, если вывести объект при помощи `alert`:
```js
//+ run
var user = {
firstName: 'Василий'
};
alert( user ); // [object Object]
```
Как видно, содержимое объекта не вывелось. Это потому, что стандартным строковым представлением пользовательского объекта является строка `"[object Object]"`.
Такой вывод объекта не содержит интересной информации. Поэтому имеет смысл его поменять на что-то более полезное.
**Если в объекте присутствует метод `toString`, который возвращает примитив, то он используется для преобразования.**
```js
//+ run
var user = {
firstName: 'Василий',
*!*toString:*/!* function() {
return 'Пользователь ' + this.firstName;
}
};
alert( user ); // Пользователь Василий
```
[smart header="Результатом `toString` может быть любой примитив"]
Метод `toString` не обязан возвращать именно строку.
Его результат может быть любого примитивного типа. Например, это может быть число, как в примере ниже:
```js
//+ run
var obj = {
toString: function() {
return 123;
}
};
alert( obj ); // 123
```
Поэтому мы и называем его здесь *"строковое преобразование"*, а не "преобразование к строке".
[/smart]
Все объекты, включая встроенные, имеют свои реализации метода `toString`, например:
```js
//+ run
alert( [1, 2] ); // toString для массивов выводит список элементов "1,2"
alert( new Date ); // toString для дат выводит дату в виде строки
alert( function() {} ); // toString для функции выводит её код
```
## Численное преобразование
Для численного преобразования объекта используется метод `valueOf`, а если его нет -- то `toString`:
```js
//+ run
var room = {
number: 777,
valueOf: function() { return this.number; },
toString: function() { return this.number; }
};
alert( +room ); // 777, *!*вызвался valueOf*/!*
delete room.valueOf; // *!*valueOf удалён*/!*
alert( +room ); // 777, *!*вызвался toString*/!*
```
Метод `valueOf` обязан возвращать примитивное значение, иначе его результат будет проигнорирован. При этом -- не обязательно числовое.
[smart header="У большинства объектов нет `valueOf`"]
У большинства встроенных объектов такого `valueOf` нет, поэтому численное и строковое преобразования для них работают одинаково.
Исключением является объект `Date`, который поддерживает оба типа преобразований:
```js
//+ run
alert( new Date() ); // toString: Дата в виде читаемой строки
alert( +new Date() ); // valueOf: кол-во миллисекунд, прошедших с 01.01.1970
```
[/smart]
[smart header="Детали спецификации"]
Если посмотреть в стандарт, то в пункте [15.2.4.4](http://es5.github.com/x15.2.html#x15.2.4.4) говорится о том, что `valueOf` есть у любых объектов. Но он ничего не делает, просто возвращает сам объект (не-примитивное значение!), а потому игнорируется.
[/smart]
## Две стадии преобразования
Итак, объект преобразован в примитив при помощи `toString` или `valueOf`.
Но на этом преобразования не обязательно заканчиваются. Вполне возможно, что в процессе вычислений этот примитив будет преобразован во что-то другое.
Например, рассмотрим применение к объекту операции `==`:
```js
//+ run
var obj = {
valueOf: function() {
return 1;
}
};
alert( obj == true ); // true
```
Объект `obj` был сначала преобразован в примитив, используя численное преобразование, получилось `1 == true`.
Далее, так как значения всё ещё разных типов, применяются правила преобразования примитивов, результат: `true`.
То же самое -- при сложении с объектом при помощи `+`:
```js
//+ run
var obj = {
valueOf: function() {
return 1;
}
};
alert( obj + "test" ); // 1test
```
Или вот, для разности объектов:
```js
//+ run
var a = {
valueOf: function() {
return "1";
}
};
var b = {
valueOf: function() {
return "2";
}
};
alert( a + b ); // "12"
alert( a - b ); // "1" - "2" = -1
```
[warn header="Исключение: `Date`"]
Объект `Date`, по историческим причинам, является исключением.
Бинарный оператор плюс `+` обычно использует числовое преобразование и метод `valueOf`. Как мы уже знаем, если подходящего `valueOf` нет (а его нет у большинства объектов), то используется `toString`, так что в итоге преобразование происходит к строке. Но если есть `valueOf`, то используется `valueOf`. Выше в примере как раз `a + b` это демонстрируют.
У объектов `Date` есть и `valueOf` -- возвращает количество миллисекунд, и `toString` -- возвращает строку с датой.
...Но оператор `+` для `Date` использует именно `toString` (хотя должен бы `valueOf`).
Это и есть исключение:
```js
//+ run
// бинарный плюс для даты toString, для остальных объектов valueOf
alert( new Date + "" ); // "строка даты"
```
Других подобных исключений нет.
[/warn]
[warn header="Как испугать Java-разработчика"]
В языке Java (это не JavaScript, другой язык, здесь приведён для примера) логические значения можно создавать, используя синтаксис `new Boolean(true/false)`, например `new Boolean(true)`.
В JavaScript тоже есть подобная возможность, которая возвращает "объектную обёртку" для логического значения.
Эта возможность давно существует лишь для совместимости, она и не используется на практике, поскольку приводит к странным результатам. Некоторые из них могут сильно удивить человека, не привыкшего к JavaScript, например:
```js
//+ run
var value = new Boolean(false);
if (value) {
alert( true ); // сработает!
}
```
Почему запустился `alert`? Ведь в `if` находится `false`... Проверим:
```js
//+ run
var value = new Boolean(false);
*!*
alert( value ); // выводит false, все ок..
*/!*
if (value) {
alert( true ); // ..но тогда почему выполняется alert в if ?!?
}
```
Дело в том, что `new Boolean` -- это не примитивное значение, а объект. Поэтому в логическом контексте он преобразуется к `true`, в результате работает первый пример.
А второй пример вызывает `alert`, который преобразует объект к строке, и он становится `"false"`.
**В JavaScript вызовы `new Boolean/String/Number` не используются, а используются простые вызовы соответствующих функций, они преобразуют значение в примитив нужного типа, например `Boolean(val) === !!val`.**
[/warn]
## Итого
<ul>
<li>В логическом контексте объект -- всегда `true`.</li>
<li>При строковом преобразовании объекта используется его метод `toString`. Он должен возвращать примитивное значение, причём не обязательно именно строку.
</li>
<li>Для численного преобразования используется метод `valueOf`, который также может возвратить любое примитивное значение. У большинства объектов `valueOf` не работает (возвращает сам объект и потому игнорируется), при этом для численного преобразования используется `toString`.</li>
</ul>
Полный алгоритм преобразований есть в спецификации EcmaScript, смотрите пункты [11.8.5](http://es5.github.com/x11.html#x11.8.5), [11.9.3](http://es5.github.com/x11.html#x11.9.3), а также [9.1](http://es5.github.com/x9.html#x9.1) и [9.3](http://es5.github.com/x9.html#x9.3).
Заметим, для полноты картины, что некоторые тесты знаний в интернет предлагают вопросы типа:
```js
//+ no-beautify
{}[0] // чему равно?
{} + {} // а так?
```
Если вы запустите эти выражения в консоли, то результат может показаться странным. Подвох здесь в том, что если фигурные скобки `{...}` идут не в выражении, а в основном потоке кода, то JavaScript считает, что это не объект, а "блок кода" (как `if`, `for`, но без оператора, просто группировка команд вместе, используется редко).
Вот блок кода с командой:
```js
//+run
{
alert("Блок")
}
```
А если команду изъять, то будет пустой блок `{}`, который ничего не делает. Два примера выше как раз содержат пустой блок в начале, который ничего не делает. Иначе говоря:
```js
//+ no-beautify
{}[0] // то же что и: [0]
{} + {} // то же что и: + {}
```
То есть, такие вопросы -- не на преобразование типов, а на понимание, что если `{ ... }` находится вне выражений, то это не объект, а блок.