Skip to content

Instantly share code, notes, and snippets.

@dSalieri
Last active November 3, 2023 04:28
Show Gist options
  • Save dSalieri/5e92f733d622724c70b32f7c6e823064 to your computer and use it in GitHub Desktop.
Save dSalieri/5e92f733d622724c70b32f7c6e823064 to your computer and use it in GitHub Desktop.
Как работает преобразование значений в примитив используя Symbol.toPrimitive, valueOf, toString

Очень интересная тема, на которой я давненько съел собаку при изучении. Теперь хотелось бы помочь закрыть чей-нибудь гештальт при изучении 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.

Примеры:

  1. 10 + {} Этот пример затрагивает операцию сложения двух операндов, операция ApplyStringOrNumericBinaryOperator, которая решает исход применения оператора между двумя операндами. По сути сейчас это так ApplyStringOrNumericBinaryOperator(10, +, {}). Что произойдет в этой операции расказывать долго, но коротко для данного случая. Произойдет особый случай для операции +, в которой каждый из операндов будет обработан операцией ToPrimitive, потом будет проверено является ли один из операндов строковым типом данных и если да, тогда каждый из операндов будет преобразован в строку операцией ToString. После конвертации произойдет конкатенация и возврат значения. Как исход результат этой операции равен "10[object Object]"

  2. 10 === {} Этот пример схож с предыдущим но он уже отличается своей операцией. И для проверки строгого равенства используется эта операция IsStrictlyEqual Итак сейчас это IsStrictlyEqual(10, {}). И если типы данных разные то вернется false. Ну собственно он и возвращается. Здесь вообще отсутствует конвертация, но она присутствует в нестрогих равенствах; для нестрогих сравниваний будет использоваться следующая операция IsLooselyEqual

  3. Number({}) Этот пример наверное самый простой, так как напрямую просит сконвертировать значение. Вот алгоритм Number. В нем он попытается вызвать операцию ToNumeric, которая в свою очередь вызовет ToPrimitive с подсказкой "number", после "примитивизации" будет вызвана операция ToNumber с примитивным значением и после будет возвращен результат в Number, которая вернет результат. В нашем случае результат NaN (смотреть операцию StringToNumber).

Если хотите объяснения какой-то другой операции, оставляйте комментарии, посмотрим с чем у вас трудности.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment