Очень интересная тема, на которой я давненько съел собаку при изучении. Теперь хотелось бы помочь закрыть чей-нибудь гештальт при изучении javascript.
Так как поведение внутренних механизмов изменилось со временем, то я обязан учесть этот факт. Итак начнем мы с ядра этого механизма, а затем рассмотрим частные случаи.
Алгоритм ToPrimitive(input[,preferredType]):
The abstract operation ToPrimitive takes argument input (an ECMAScript language value) and optional argument preferredType (string or number). It converts its input argument to a non-Object type. If an object is capable of converting to more than one primitive type, it may use the optional hint preferredType to favour that type. It performs the following steps when called:
1. If Type(input) is Object, then
a. Let exoticToPrim be ? GetMethod(input, @@toPrimitive).
b. If exoticToPrim is not undefined, then
i. If preferredType is not present, let hint be "default".
ii. Else if preferredType is string, let hint be "string".
iii. Else,
1. Assert: preferredType is number.
2. Let hint be "number".
iv. Let result be ? Call(exoticToPrim, input, « hint »).
v. If Type(result) is not Object, return result.
vi. Throw a TypeError exception.
c. If preferredType is not present, let preferredType be number.
d. Return ? OrdinaryToPrimitive(input, preferredType).
2. Return input.
NOTE When ToPrimitive is called without a hint, then it generally behaves as if the hint were number. However, objects may over-ride this behaviour by defining a @@toPrimitive method. Of the objects defined in this specification only Dates (see 21.4.4.45) and Symbol objects (see 20.4.3.5) over-ride the default ToPrimitive behaviour. Dates treat the absence of a hint as if the hint were string.
Перевод:
Абстрактная операция ToPrimitive принимает аргумент input (значение языка ECMAScript) и необязательный аргумент preferredType (строка или число). Она конвертирует его input аргумент в не-объектный тип данных. Если объект способен преобразовываться к более чем одному примитивному типу, он может использовать необязательную подсказку из preferredType в пользу этого типа
1. Если Type(input) это тип Object, тогда
a. Пусть exoticToPrim будет результатом операции GetMethod(input, @@toPrimitive)
b. Если exoticToPrim это не значение undefined, тогда
i. Если preferredType не присутствует, пусть hint будет "default"
ii. Иначе если preferredType это строка, пусть hint будет "string"
iii. Иначе,
1. Утверждаю: preferredType это число
2. Пусть hint будет "number"
iv. Пусть result будет результатом операции Call(exoticToPrim, input, « hint »)
v. Если Type(result) это не тип Object, вернуть result
vi. Бросить исключение TypeError
c. Если preferredType не присутствует, пусть preferredType будет числом
d. Вернуть OrdinaryToPrimitive(input, preferredType)
2. Вернуть input
Примечание Когда ToPrimitive вызывается без подсказки, тогда она в общем ведет как если подсказка была числом. Однако, объекты могут перезаписывать это поведение определяя @@toPrimitive метод. Объекты определенные в этой спецификации только Dates (смотри 21.4.4.45) и Symbol объекты (смотри 20.4.3.5) перезаписывают поведение ToPrimitive по-умолчанию. Dates обрабатывают отсутствие подсказки как если подсказка была строкой.
Пояснение:
- Шаг
1.a
пытается получить свойствоSymbol.toPrimitive
у нашего объекта; записывает вexoticToPrim
. - Шаг
2.b
если в шаге1.a
мы получили неundefined
приступаем к плану где выясняем какую подсказку (hint
) нам использовать. ЕслиpreferredType
отсутствует тогдаhint
равен"default"
, иначе еслиpreferredType
строка тогдаhint
равен"string"
, иначеhint
равен"number"
. - Шаг
1.b.iv
вызывает внутреннюю функциюCall
c агрументамиexoticToPrim, input, «hint»
, что по сути соответствует такому кодуexoticToPrim.call(input, hint)
, если результатом выполнения этой операции будет объект, код выбросит ошибку в противном случае вернется примитив. Если все же мы получилиexoticToPrim
равнымundefined
, значит мы не выполняли ветку1.b
и это значит что мы идем выполнять1.c
и1.d
. - Шаг
1.c
проверяетpreferredType
, если он не присутствует, тогдаpreferredType
будет равен"number"
. - Шаг
1.d
выполняет операциюOrdinaryToPrimitive
с аргументамиinput, preferredType
; выполнение этой операции вернет примитив либо вызовет ошибку; эта операция как раз использует методыvalueOf
иtoString
.
Алгоритм OrdinaryToPrimitive(O, hint):
1. If hint is string, then
a. Let methodNames be « "toString", "valueOf" ».
2. Else,
a. Let methodNames be « "valueOf", "toString" ».
3. For each element name of methodNames, do
a. Let method be ? Get(O, name).
b. If IsCallable(method) is true, then
i. Let result be ? Call(method, O).
ii. If Type(result) is not Object, return result.
4. Throw a TypeError exception.
Перевод:
1. Если hint это строка, тогда
a. Пусть methodNames будет « "toString", "valueOf" »
2. Иначе,
a. Пусть methodNames будет « "valueOf", "toString" »
3. Для каждого элемента name из methodNames, do
a. Пусть method будет результатом операции Get(O, name)
b. Если IsCallable(method) равно true, тогда
i. Пусть result будет результатом операции Call(method, O)
ii. Если результат операции Type(result) не равен типу Object, вернуть result
4. Бросить исключение TypeError
Пояснение: Если hint
имеет значение "string"
тогда список методов определяется как «"toString", "valueOf"»
в противном случае «"valueOf", "toString"»
. Далее в цикле по списку имен методов мы пытаемся получить один из них и вызвать, второй элемент не будет перечислен если на первом элементе произойдет все необходимое. И шаг 4 произойдет в том случае если ни один методов из списка не будет найдет; такую ошибку можно спровоцировать таким кодом Number(Object.create(null))
Резюме: ToPrimitive
- это внешнее ядро преобразования к примитиву, его цель попытаться использовать Symbol.toPrimitive
и если не получится использовать подядро - OrdinaryToPrimitive
, которое в свою очередь использует методы valueOf
и toString
.
Примеры:
-
10 + {}
Этот пример затрагивает операцию сложения двух операндов, операция ApplyStringOrNumericBinaryOperator, которая решает исход применения оператора между двумя операндами. По сути сейчас это такApplyStringOrNumericBinaryOperator(10, +, {})
. Что произойдет в этой операции расказывать долго, но коротко для данного случая. Произойдет особый случай для операции+
, в которой каждый из операндов будет обработан операциейToPrimitive
, потом будет проверено является ли один из операндов строковым типом данных и если да, тогда каждый из операндов будет преобразован в строку операциейToString
. После конвертации произойдет конкатенация и возврат значения. Как исход результат этой операции равен"10[object Object]"
-
10 === {}
Этот пример схож с предыдущим но он уже отличается своей операцией. И для проверки строгого равенства используется эта операция IsStrictlyEqual Итак сейчас этоIsStrictlyEqual(10, {})
. И если типы данных разные то вернетсяfalse
. Ну собственно он и возвращается. Здесь вообще отсутствует конвертация, но она присутствует в нестрогих равенствах; для нестрогих сравниваний будет использоваться следующая операция IsLooselyEqual -
Number({})
Этот пример наверное самый простой, так как напрямую просит сконвертировать значение. Вот алгоритм Number. В нем он попытается вызвать операциюToNumeric
, которая в свою очередь вызоветToPrimitive
с подсказкой"number"
, после "примитивизации" будет вызвана операцияToNumber
с примитивным значением и после будет возвращен результат вNumber
, которая вернет результат. В нашем случае результатNaN
(смотреть операцию StringToNumber).
Если хотите объяснения какой-то другой операции, оставляйте комментарии, посмотрим с чем у вас трудности.