Что происходит, когда объекты добавляются obj1 + obj2
, вычитаются obj1 - obj2
или печатаются с помощью alert(obj)
?
JavaScript не позволяет настраивать работу операторов с объектами. В отличие от некоторых других языков программирования, таких как Ruby или C++, мы не можем реализовать специальный объектный метод для обработки сложения (или других операторов).
В случае таких операций объекты автоматически преобразуются в примитивы, а затем операция выполняется над этими примитивами и приводит к получению примитивного значения.
Это важное ограничение: результат obj1 + obj2
(или другой математической операции) не может быть другим объектом!
Например, мы не можем создавать объекты, представляющие векторы или матрицы (или достижения или что-то еще), складывать их и ожидать в качестве результата «суммируемый» объект. Подобные архитектурные подвиги автоматически исключаются из рассмотрения.
Итак, поскольку мы технически не можем здесь многое сделать, в реальных проектах с объектами нет никакой математики. Когда такое случается, за редким исключением, это происходит из-за ошибки в кодировании.
В этой главе мы рассмотрим, как объект преобразуется в примитив и как его настроить.
У нас есть две цели:
Date
). Мы встретим их позже.В главе «Преобразования типов» мы рассмотрели правила числовых, строковых и логических преобразований примитивов. Но мы оставили пробел для предметов. Теперь, когда мы знаем о методах и символах, появляется возможность его заполнить.
true
в логическом контексте, вот и все. Существуют только числовые и строковые преобразования.Date
(которые будут рассмотрены в главе «Дата и время») могут быть вычтены, и результатом date1 - date2
будет разница во времени между двумя датами.alert(obj)
и в подобных контекстах.Преобразование строк и чисел мы можем реализовать самостоятельно, используя специальные методы объекта.
Теперь давайте углубимся в технические детали, потому что это единственный способ подробно осветить тему.
Как JavaScript решает, какое преобразование применить?
Существует три варианта преобразования типов, которые происходят в различных ситуациях. Они называются «подсказками», как описано в спецификации:
"string"
Для преобразования объекта в строку, когда мы выполняем операцию над объектом, который ожидает строку, например alert
:
// output alert(obj); // using object as a property key anotherObj[obj] = 123;
"number"
Для преобразования объекта в число, например, когда мы занимаемся математикой:
// explicit conversion let num = Number(obj); // maths (except binary plus) let n = +obj; // unary plus let delta = date1 - date2; // less/greater comparison let greater = user1 > user2;
Большинство встроенных математических функций также включают такое преобразование.
"default"
Возникает в редких случаях, когда оператор «не уверен», какой тип ожидать.
Например, двоичный плюс +
может работать как со строками (объединяет их), так и с числами (складывает). Таким образом, если двоичный плюс получает объект в качестве аргумента, для его преобразования используется подсказка "default"
.
Кроме того, если объект сравнивается с помощью ==
со строкой, числом или символом, также неясно, какое преобразование следует выполнить, поэтому используется подсказка "default"
.
// binary plus uses the "default" hint let total = obj1 + obj2; // obj == number uses the "default" hint if (user == 1) { ... };
Операторы сравнения «больше» и «меньше», такие как <
>
, также могут работать как со строками, так и с числами. Тем не менее, они используют подсказку "number"
, а не "default"
. Это по историческим причинам.
Однако на практике все немного проще.
Все встроенные объекты, за исключением одного случая (объекта Date
, мы узнаем об этом позже), реализуют преобразование "default"
так же, как и "number"
. И нам, наверное, следует сделать то же самое.
Тем не менее, важно знать обо всех трёх подсказках, скоро мы увидим, почему.
Чтобы выполнить преобразование, JavaScript пытается найти и вызвать три метода объекта:
obj[Symbol.toPrimitive](hint)
– метода с символьным ключом Symbol.toPrimitive
(системный символ), если такой метод существует,"string"
obj.toString()
или obj.valueOf()
, что бы там ни было."number"
или "default"
obj.valueOf()
или obj.toString()
, что бы там ни было. Начнем с первого способа. Существует встроенный символ с именем Symbol.toPrimitive
, который следует использовать для имени метода преобразования, например:
obj[Symbol.toPrimitive] = function(hint) { // here goes the code to convert this object to a primitive // it must return a primitive value // hint = one of "string", "number", "default" };
Если метод Symbol.toPrimitive
существует, он используется для всех подсказок, и больше никаких методов не требуется.
Например, здесь это реализует user
объект:
let user = { name: "John", money: 1000, [Symbol.toPrimitive](hint) { alert(`hint: ${hint}`); return hint == "string" ? `{name: "${this.name}"}` : this.money; } }; // conversions demo: alert(user); // hint: string -> {name: "John"} alert(+user); // hint: number -> 1000 alert(user + 500); // hint: default -> 1500
Как мы видим из кода, user
становится строкой с описанием или денежной суммой, в зависимости от конверсии. Единственный метод user[Symbol.toPrimitive]
обрабатывает все случаи преобразования.
Если Symbol.toPrimitive
нет, JavaScript пытается найти методы toString
и valueOf
:
"string"
: вызовите метод toString
, и если он не существует или возвращает объект вместо примитивного значения, затем вызовите valueOf
(так что toString
имеет приоритет для преобразований строк).valueOf
, и если он не существует или возвращает объект вместо примитивного значения, затем вызовите toString
(так что valueOf
имеет приоритет для математических вычислений). Методы toString
и valueOf
появились еще в древности. Это не символы (так давно символов не существовало), а скорее «обычные» методы со строковыми именами. Они предоставляют альтернативный «старый стиль» способа реализации преобразования.
Эти методы должны возвращать примитивное значение. Если toString
или valueOf
возвращает объект, он игнорируется (так же, как если бы метода не было).
По умолчанию простой объект имеет следующие методы toString
и valueOf
:
toString
возвращает строку "[object Object]"
.valueOf
возвращает сам объект.Вот демо:
let user = {name: "John"}; alert(user); // [object Object] alert(user.valueOf() === user); // true
Поэтому, если мы попытаемся использовать объект как строку, например, в alert
или около того, то по умолчанию мы увидим [object Object]
.
valueOf
по умолчаниюOf упоминается здесь только для полноты картины, чтобы избежать путаницы. Как видите, он возвращает сам объект и поэтому игнорируется. Не спрашивайте меня, почему, это по историческим причинам. Поэтому мы можем предположить, что его не существует.
Давайте реализуем эти методы для настройки преобразования.
Например, здесь user
делает то же самое, что и выше, используя комбинацию toString
и valueOf
вместо Symbol.toPrimitive
:
let user = { name: "John", money: 1000, // for hint="string" toString() { return `{name: "${this.name}"}`; }, // for hint="number" or "default" valueOf() { return this.money; } }; alert(user); // toString -> {name: "John"} alert(+user); // valueOf -> 1000 alert(user + 500); // valueOf -> 1500
Как мы видим, поведение такое же, как и в предыдущем примере с Symbol.toPrimitive
.
Часто нам нужно одно универсальное место для обработки всех примитивных преобразований. В этом случае мы можем реализовать только toString
, вот так:
let user = { name: "John", toString() { return this.name; } }; alert(user); // toString -> John alert(user + 500); // toString -> John500
В отсутствие Symbol.toPrimitive
и valueOf
toString
будет обрабатывать все примитивные преобразования.
Обо всех методах преобразования примитивов важно знать, что они не обязательно возвращают «подсказанный» примитив.
Невозможно контролировать, возвращает ли toString
именно строку или возвращает ли метод Symbol.toPrimitive
число для подсказки "number"
.
Единственное обязательное условие: эти методы должны возвращать примитив, а не объект.
По историческим причинам, если toString
или valueOf
возвращает объект, ошибки нет, но такое значение игнорируется (как если бы метод не существовал). Это потому, что в древние времена в JavaScript не было хорошей концепции «ошибки».
Напротив, Symbol.toPrimitive
более строгий, он должен возвращать примитив, иначе возникнет ошибка.
Как мы уже знаем, многие операторы и функции выполняют преобразования типов, например, умножение *
преобразует операнды в числа.
Если мы передаем объект в качестве аргумента, то происходит два этапа вычислений:
Например:
let obj = { // toString handles all conversions in the absence of other methods toString() { return "2"; } }; alert(obj * 2); // 4, object converted to primitive "2", then multiplication made it a number
obj * 2
сначала преобразует объект в примитив (это строка "2"
)."2" * 2
становится 2 * 2
(строка преобразуется в число).Двоичный плюс объединяет строки в той же ситуации, поскольку он с радостью принимает строку:
let obj = { toString() { return "2"; } }; alert(obj + 2); // "22" ("2" + 2), conversion to primitive returned a string => concatenation
Преобразование объекта в примитив вызывается автоматически многими встроенными функциями и операторами, которые ожидают примитив в качестве значения.
Существует 3 его вида (подсказки):
"string"
(для alert
и других операций, которым нужна строка)"number"
(для математики)"default"
(немногие операторы, обычно объекты реализуют его так же, как "number"
)Спецификация явно описывает, какой оператор какую подсказку использует.
Алгоритм конвертации:
obj[Symbol.toPrimitive](hint)
если метод существует,"string"
obj.toString()
или obj.valueOf()
, что бы там ни было."number"
или "default"
obj.valueOf()
или obj.toString()
, что бы там ни было.Все эти методы должны возвращать работающий примитив (если он определен).
На практике часто достаточно реализовать только obj.toString()
как «универсальный» метод для преобразований строк, который должен возвращать «удобочитаемое» представление объекта для целей ведения журнала или отладки.