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

13 KiB
Raw Blame History

Преобразование объектов: toString и valueOf

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

Бывают операции, при которых объект должен быть преобразован в примитив. [cut] Например:

  • Строковое преобразование -- если объект выводится через `alert(obj)`.
  • Численное преобразование -- при арифметических операциях, сравнении с примитивом.
  • Логическое преобразование -- при `if(obj)` и других логических операциях.

Рассмотрим эти преобразования по очереди.

Логическое преобразование

Проще всего -- с логическим преобразованием.

Любой объект в логическом контексте -- true, даже если это пустой массив [] или объект {}.

//+ run
if ({} && []) {
  alert( "Все объекты - true!" ); // alert сработает
}

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

Строковое преобразование проще всего увидеть, если вывести объект при помощи alert:

//+ run
var user = {
  firstName: 'Василий'
};

alert( user ); // [object Object]

Как видно, содержимое объекта не вывелось. Это потому, что стандартным строковым представлением пользовательского объекта является строка "[object Object]".

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

Если в объекте присутствует метод toString, который возвращает примитив, то он используется для преобразования.

//+ run
var user = {

  firstName: 'Василий',

  *!*toString:*/!* function() {
    return 'Пользователь ' + this.firstName;
  }
};

alert( user );  // Пользователь Василий

[smart header="Результатом toString может быть любой примитив"] Метод toString не обязан возвращать именно строку.

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

//+ run
var obj = {
  toString: function() {
    return 123;
  }
};

alert( obj ); // 123

Поэтому мы и называем его здесь "строковое преобразование", а не "преобразование к строке". [/smart]

Все объекты, включая встроенные, имеют свои реализации метода toString, например:

//+ run
alert( [1, 2] ); // toString для массивов выводит список элементов "1,2"
alert( new Date ); // toString для дат выводит дату в виде строки
alert( function() {} ); // toString для функции выводит её код

Численное преобразование

Для численного преобразования объекта используется метод valueOf, а если его нет -- то toString:

//+ 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, который поддерживает оба типа преобразований:

//+ run
alert( new Date() ); // toString: Дата в виде читаемой строки
alert( +new Date() ); // valueOf: кол-во миллисекунд, прошедших с 01.01.1970

[/smart]

[smart header="Детали спецификации"] Если посмотреть в стандарт, то в пункте 15.2.4.4 говорится о том, что valueOf есть у любых объектов. Но он ничего не делает, просто возвращает сам объект (не-примитивное значение!), а потому игнорируется. [/smart]

Две стадии преобразования

Итак, объект преобразован в примитив при помощи toString или valueOf.

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

Например, рассмотрим применение к объекту операции ==:

//+ run
var obj = {
  valueOf: function() {
    return 1;
  }
};

alert( obj == true ); // true

Объект obj был сначала преобразован в примитив, используя численное преобразование, получилось 1 == true.

Далее, так как значения всё ещё разных типов, применяются правила преобразования примитивов, результат: true.

То же самое -- при сложении с объектом при помощи +:

//+ run
var obj = {
  valueOf: function() {
    return 1;
  }
};

alert( obj + "test" ); // 1test

Или вот, для разности объектов:

//+ 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).

Это и есть исключение:

//+ run
// бинарный плюс для даты toString, для остальных объектов valueOf
alert( new Date + "" ); // "строка даты"

Других подобных исключений нет. [/warn]

[warn header="Как испугать Java-разработчика"] В языке Java (это не JavaScript, другой язык, здесь приведён для примера) логические значения можно создавать, используя синтаксис new Boolean(true/false), например new Boolean(true).

В JavaScript тоже есть подобная возможность, которая возвращает "объектную обёртку" для логического значения.

Эта возможность давно существует лишь для совместимости, она и не используется на практике, поскольку приводит к странным результатам. Некоторые из них могут сильно удивить человека, не привыкшего к JavaScript, например:

//+ run
var value = new Boolean(false);
if (value) {
  alert( true ); // сработает!
}

Почему запустился alert? Ведь в if находится false... Проверим:

//+ 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]

Итого

  • В логическом контексте объект -- всегда `true`.
  • При строковом преобразовании объекта используется его метод `toString`. Он должен возвращать примитивное значение, причём не обязательно именно строку.
  • Для численного преобразования используется метод `valueOf`, который также может возвратить любое примитивное значение. У большинства объектов `valueOf` не работает (возвращает сам объект и потому игнорируется), при этом для численного преобразования используется `toString`.

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

Заметим, для полноты картины, что некоторые тесты знаний в интернет предлагают вопросы типа:

//+ no-beautify
{}[0]  // чему равно? 
{} + {} // а так?

Если вы запустите эти выражения в консоли, то результат может показаться странным. Подвох здесь в том, что если фигурные скобки {...} идут не в выражении, а в основном потоке кода, то JavaScript считает, что это не объект, а "блок кода" (как if, for, но без оператора, просто группировка команд вместе, используется редко).

Вот блок кода с командой:

//+run
{
  alert("Блок")
}

А если команду изъять, то будет пустой блок {}, который ничего не делает. Два примера выше как раз содержат пустой блок в начале, который ничего не делает. Иначе говоря:

//+ no-beautify
{}[0]   // то же что и: [0]  
{} + {} // то же что и: + {}

То есть, такие вопросы -- не на преобразование типов, а на понимание, что если { ... } находится вне выражений, то это не объект, а блок.