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

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

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

Например:

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

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

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

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

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

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

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

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

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

alert( user ); // [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; // valueOf удалён

alert( +room );  // 777, вызвался toString

Метод valueOf обязан возвращать примитивное значение, иначе его результат будет проигнорирован. При этом – не обязательно числовое.

У большинства объектов нет valueOf

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

Исключением является объект Date, который поддерживает оба типа преобразований:

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

Если посмотреть в стандарт, то в пункте 15.2.4.4 говорится о том, что valueOf есть у любых объектов. Но он ничего не делает, просто возвращает сам объект (не-примитивное значение!), а потому игнорируется.

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

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

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

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

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

alert( obj == true ); // true

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

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

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

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

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

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

var a = {
  valueOf: function() {
    return "1";
  }
};
var b = {
  valueOf: function() {
    return "2";
  }
};

alert( a + b ); // "12"
alert( a - b ); // "1" - "2" = -1
Исключение: Date

Объект Date, по историческим причинам, является исключением.

Бинарный оператор плюс + обычно использует числовое преобразование и метод valueOf. Как мы уже знаем, если подходящего valueOf нет (а его нет у большинства объектов), то используется toString, так что в итоге преобразование происходит к строке. Но если есть valueOf, то используется valueOf. Выше в примере как раз a + b это демонстрируют.

У объектов Date есть и valueOf – возвращает количество миллисекунд, и toString – возвращает строку с датой.

…Но оператор + для Date использует именно toString (хотя должен бы valueOf).

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

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

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

Как испугать Java-разработчика

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

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

Эта возможность давно существует лишь для совместимости, она и не используется на практике, поскольку приводит к странным результатам. Некоторые из них могут сильно удивить человека, не привыкшего к 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".

В JavaScript вызовы new Boolean/String/Number не используются, а используются простые вызовы соответствующих функций, они преобразуют значение в примитив нужного типа, например Boolean(val) === !!val.

Итого

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

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

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

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

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

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

{
  alert("Блок")
}

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

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

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

Задачи

важность: 5

Почему результат true ?

alert( ['x'] == 'x' );

Если с одной стороны – объект, а с другой – нет, то сначала приводится объект.

В данном случае сравнение означает численное приведение. У массивов нет valueOf, поэтому вызывается toString, который возвращает список элементов через запятую.

В данном случае, элемент только один – он и возвращается. Так что ['x'] становится 'x'. Получилось 'x' == 'x', верно.

P.S. По той же причине верны равенства:

alert( ['x', 'y'] == 'x,y' ); // true
alert( [] == '' ); // true
важность: 5

Объявлен объект с toString и valueOf.

Какими будут результаты alert?

var foo = {
  toString: function() {
    return 'foo';
  },
  valueOf: function() {
    return 2;
  }
};

alert( foo );
alert( foo + 1 );
alert( foo + "3" );

Подумайте, прежде чем ответить.

Первый alert(foo)

Возвращает строковое представление объекта, используя toString, т.е. "foo".

Второй alert(foo + 1)

Оператор '+' преобразует объект к примитиву, используя valueOf, так что результат: 3.

Третий alert(foo + „3“)

То же самое, что и предыдущий случай, объект превращается в примитив 2. Затем происходит сложение 2 + '3'. Оператор '+' при сложении чего-либо со строкой приводит и второй операнд к строке, а затем применяет конкатенацию, так что результат – строка "23".

важность: 5

Почему первое равенство – неверно, а второе – верно?

alert( [] == [] ); // false
alert( [] == ![] ); // true

Какие преобразования происходят при вычислении?

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

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

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

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

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

    alert( [] == false );
  2. Проверка равенства между объектом и примитивом вызывает численное преобразование объекта.

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

    alert( '' == false );
  3. Сравнение различных типов вызывает численное преобразование слева и справа:

    alert( 0 == 0 );

    Теперь результат очевиден.

важность: 5

Подумайте, какой результат будет у выражений ниже. Когда закончите – сверьтесь с решением.

new Date(0) - 0
new Array(1)[0] + ""
({})[0]
[1] + 1
[1,2] + [3,4]
[] + null + 1
[[0]][0][0]
({} + {})
new Date(0) - 0 = 0 // (1)
new Array(1)[0] + "" = "undefined" // (2)
({})[0] = undefined // (3)
[1] + 1 = "11" // (4)
[1,2] + [3,4] = "1,23,4" // (5)
[] + null + 1 = "null1" // (6)
[[0]][0][0] = 0 // (7)
({} + {}) = "[object Object][object Object]" // (8)
  1. new Date(0) – дата, созданная по миллисекундам и соответствующая 0 мс от 1 января 1970 года 00:00:00 UTC. Оператор минус - преобразует дату обратно в число миллисекунд, то есть в 0.

  2. new Array(num) при вызове с единственным аргументом-числом создаёт массив данной длины, без элементов. Поэтому его нулевой элемент равен undefined, при сложении со строкой получается строка "undefined".

  3. Фигурные скобки – это создание пустого объекта, у него нет свойства '0'. Так что значением будет undefined. Обратите внимание на внешние, круглые скобки. Если их убрать и запустить {}[0] в отладочной консоли браузера – будет 0, т.к. скобки {} будут восприняты как пустой блок кода, после которого идёт массив.

  4. Массив преобразуется в строку "1". Оператор "+" при сложении со строкой приводит второй аргумент к строке – значит будет "1" + "1" = "11".

  5. Массивы приводятся к строке и складываются.

  6. Массив преобразуется в пустую строку "" + null + 1, оператор "+" видит, что слева строка и преобразует null к строке, получается "null" + 1, и в итоге "null1".

  7. [[0]] – это вложенный массив [0] внутри внешнего [ ]. Затем мы берём от него нулевой элемент, и потом еще раз.

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

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

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

  8. Каждый объект преобразуется к примитиву. У встроенных объектов Object нет подходящего valueOf, поэтому используется toString, так что складываются в итоге строковые представления объектов.

важность: 2

Напишите функцию sum, которая будет работать так:

sum(1)(2) == 3; // 1 + 2
sum(1)(2)(3) == 6; // 1 + 2 + 3
sum(5)(-1)(2) == 6
sum(6)(-1)(-2)(-3) == 0
sum(0)(1)(2)(3)(4)(5) == 15

Количество скобок может быть любым.

Пример такой функции для двух аргументов – есть в решении задачи Сумма через замыкание.

Подсказка

Чтобы sum(1), а также sum(1)(2) можно было вызвать новыми скобками – результатом sum должна быть функция.

Но эта функция также должна уметь превращаться в число. Для этого нужно дать ей соответствующий valueOf. А если мы хотим, чтобы и в строковом контексте она вела себя так же – то toString.

Решение

Функция, которая возвращается sum, должна накапливать значение при каждом вызове.

Удобнее всего хранить его в замыкании, в переменной currentSum. Каждый вызов прибавляет к ней очередное значение:

function sum(a) {

  var currentSum = a;

  function f(b) {
    currentSum += b;
    return f;
  }

  f.toString = function() {
    return currentSum;
  };

  return f;
}

alert( sum(1)(2) ); // 3
alert( sum(5)(-1)(2) ); // 6
alert( sum(6)(-1)(-2)(-3) ); // 0
alert( sum(0)(1)(2)(3)(4)(5) ); // 15

При внимательном взгляде на решение легко заметить, что функция sum срабатывает только один раз. Она возвращает функцию f.

Затем, при каждом запуске функция f добавляет параметр к сумме currentSum, хранящейся в замыкании, и возвращает сама себя.

В последней строчке f нет рекурсивного вызова.

Вот так была бы рекурсия:

function f(b) {
  currentSum += b;
  return f(); // <-- подвызов
}

А в нашем случае, мы просто возвращаем саму функцию, ничего не вызывая.

function f(b) {
  currentSum += b;
  return f; // <-- не вызывает сама себя, а возвращает ссылку на себя
}

Эта f используется при следующем вызове, опять возвратит себя, и так сколько нужно раз. Затем, при использовании в строчном или численном контексте – сработает toString, который вернет текущую сумму currentSum.

Карта учебника

Комментарии

перед тем как писать…
  • Приветствуются комментарии, содержащие дополнения и вопросы по статье, и ответы на них.
  • Для одной строки кода используйте тег <code>, для нескольких строк кода — тег <pre>, если больше 10 строк — ссылку на песочницу (plnkr, JSBin, codepen…)
  • Если что-то непонятно в статье — пишите, что именно и с какого места.