Mystery:

console.log(a==1 && a==2) // true
Copy the code

????? Why can a variable be equal to multiple values at the same time? Let’s look at the pre-knowledge

Read this article and you will learn

  • Implicit conversion rules for object type operations
  • The ECMAScript specification understands conversion rules for different cases of object types
  • The working principles and scenarios of ToPrimitive, OrdinaryToPrimitive, @@Toprimitive functions in the ECMAScript specification
  • Object toString, valueOf, Symbol. ToPrimitive methods and principles

ToPrimitive

The ToPrimitive method was described earlier in “Implicit Transformations that Cause headaches? There must be a Trace.” It is used to convert a complex data type to a primitive data type.

sec-toprimitive

Let’s take a closer look at the ECMAScript definition of this method

ToPrimitive ( input [ , PreferredType ] )

The abstract operation ToPrimitive takes an input argument and an optional argument PreferredType. The abstract operation ToPrimitive 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. Conversion occurs according to the following algorithm:

The abstract operation ToPrimitive takes an input parameter and an optional PreferredType parameter. The abstract operation ToPrimitive converts its input parameter to a non-object type. If an object can be converted to more than one primitive type, it can use the optional hint PreferredType to support that type. Convert according to the following algorithm:

  1. Assert: input is an ECMAScript language value.

  2. If Type (input) is Object, then

    1. If PreferredType is not present, let hint be “default”.

    2. Else if PreferredType is hint String, let hint be “string”.

    3. Else,

      1. Assert: PreferredType is hint Number.

      2. Let hint be “number”.

    4. Let exoticToPrim be ? GetMethod(input, @@toPrimitive).

    5. If exoticToPrim is not undefined, then

      1. Let result be ? Call(exoticToPrim, input, « hint »).

      2. If Type(result) is not Object, return result.

      3. Throw a TypeError exception.

    6. If hint is “default”, set hint to “number”.

    7. Return ? OrdinaryToPrimitive(input, hint).

  3. Return input.

The definition of this method is simple, taking an input parameter of the object to be converted and an optional parameter, PreferredType, that describes the target type of the conversion. PreferredType is a type, and the options are String and Number

Primitive values: Values of primitive types, including NULL, undefined, number, string, Boolean, excluding BigInt and Symbol

  1. If input is not the original value

    1. Hint “default” if no PreferredType is passed

    2. Otherwise, if the PreferredType is String, hint is “String “.

    3. Otherwise, hint is “number”.

    4. If input[@@toprimitive] method is defined

      1. Calls to the input [@ @ toPrimitive] (hint); If a raw value is returned, JavaScript returns it. 1. Otherwise, JavaScript throws a TypeError exception.
    5. Hint is “default”, set hint to “number”.

    6. Return OrdinaryToPrimitive (input, hint)

  2. Otherwise, return input without any conversion.

We will introduce @@toprimitive and OrdinaryToPrimitive methods later. Returns it if the return value is a primitive value, otherwise raises a TypeError exception

Object Type in the ECMAScript specification refers to data types other than undefined, NULL, Number, Boolean, and String. Includes BigInt and Symbol.

Values of the types undefined, NULL, number, Boolean, and string are also called primitive values

OrdinaryToPrimitive

sec-ordinarytoprimitive

OrdinaryToPrimitive ( O, hint )

When the abstract operation OrdinaryToPrimitive is called with arguments O and hint, the following steps are taken:

  1. Assert: Type(O) is Object.

  2. Assert: Type(hint) is String and its value is either “string” or “number”.

  3. If hint is “string”, then

    A, Let methodNames be « “toString”, “valueOf” »

  4. Else,

    A, Let methodNames be « “valueOf”, “toString” »

  5. For each name in methodNames in List order, 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.

  6. Throw a TypeError exception.

The OrdinaryToPrimitive method takes the converted object O and expects the converted data type Hint, which is a string with either “number” or “string”

  1. If hint is “string”

    1. The valueOf method is called, and if a raw value is returned, JavaScript returns it.
    2. Otherwise, the toString method is called, and if a raw value is returned, JavaScript returns it.
    3. Otherwise, JavaScript throws a TypeError exception.
  2. Otherwise,

    1. The toString method is called, and if a raw value is returned, JavaScript returns it.
    2. Otherwise, the valueOf method is called, and if a raw value is returned, JavaScript returns it.
    3. Otherwise, JavaScript throws a TypeError exception.

@ @ toPrimitive (hint) and Symbol. ToPrimitive (hint)

Symbol.prototype [ @@toPrimitive ] ( hint )

This function is called by ECMAScript language operators to convert a Symbol object to a primitive value. The allowed values for hint are “default”, “number”, and “string”.

When the @@toPrimitive method is called with argument hint, the following steps are taken:

Return ? thisSymbolValue(this value).

The value of the “name” property of this function is “[Symbol.toPrimitive]”.

This property has the attributes { [[Writable]]: false, [[Enumerable]]: false, [[Configurable]]: true }.

The @@Toprimitive method has been mentioned in the introduction to the toPrimitive method above

When input[@@toprimitive] is defined in the toPrimitive method, input[@@toprimitive](hint) is called as the return value. The default @@toprimitive implementation is the same as OrdinaryToPrimitive(hint)

Just the first hint parameter values can be “default” | | “string” “number”, the latter hint parameter values may be “string” | “number”

So the default implementation looks like the code below

Object.prototype['@@toPrimitive'] = function(hint){
    if(hint === 'default') hint = 'number'
    return OrdinaryToPrimitive(this, hint)
}
Copy the code

But the actual implementation of the @@toprimitive method is called symbol.toprimitive. So the code above looks like this

Object.prototype[Symbol.toPrimitive] = function(hint){
    if(hint === 'default') hint = 'number'
    return OrdinaryToPrimitive(this, hint)
}
Copy the code

You can try modifying an object’s symbol.toprimitive method to override its default implementation

let obj = {
    [Symbol.toPrimitive](hint){
        console.log(hint)
        return 1}}console.log(Number(obj));

// number Returns obj[symbol.toprimitive]('number') when number (obj) obj is an object
/ / 1
Copy the code

Object conversion rules

The conversion rules for Object types were introduced in the previous article, “Implicit conversions that cause headaches? There must be a Trace.”

The unary operator +

The effect of +obj is equivalent to executing Number(obj), whose results and execution principles have been explained previously

The binary operator +

When obj1 + obj2 is calculated:

  1. lprim = ToPrimitive(obj1)
  2. rprim = ToPrimitive(obj2)
  3. If lprim is a string or rprim is a string, return the concatenation of ToString(lprim) and ToString(rprim)
  4. Returns the result of ToNumber(lprim) and ToNumber(rprim)

The comparison operator ==

We’ve already covered the type conversion rules for the == operator in our article, “Implicit conversions? They must be tractable.

When executing x == y:

  1. If x and y are of the same type:
    1. X is Undefined and returns true
    2. X is Null and returns true
    3. X is a number:
      1. X is NaN and returns false
      2. Y is NaN and returns false
      3. X is equal to y, returns true
      4. X is +0, y is -0, return true
      5. X is -0, y is +0, return true
      6. Returns false
    4. X is a string, return true for full equality, false otherwise
    5. X is a Boolean, x and y are both true or false, return true, otherwise return false
    6. X and y refer to the same object, return true, otherwise return false
  2. Return true if x is null and y is undefined
  3. Return true if x is undefined and y is null
  4. X is a number, y is a string, x == ToNumber(y)
  5. X is a string, y is a number, and ToNumber(x) == y
  6. X is a Boolean and ToNumber(x) == y
  7. Y is a Boolean, x is equal to ToNumber of y.
  8. X == ToPrimitive(y)
  9. ToPrimitive(x) == y
  10. Returns false

The toString and the valueOf

Returning non-primitive values in toString or valueOf is not recommended, as we know from the implementation of OrdinaryToPrimitive that doing so may result in TypeError being thrown

It is recommended that toString return the Stirng value and valueOf return the Number value.

The toString and valueOf methods were mentioned above in the OrdinaryToPrimitive method. When an object’s OrdinaryToPrimitive method is called, one or more of these methods must be executed.

When ToPrimitive(obj) is executed, different logic is executed depending on the PreferredType parameter, but ultimately the OrdinaryToPrimitive method is executed as the result.

Conclusion: When an object executes ToPrimitive(obj), if its @@Toprimitive method is not overridden, one or more of its toString or valueOf methods must be executed. Otherwise it must execute its @@toprimitive method.

Get into the business

With that knowledge and conclusion in mind, let’s take a look at the previous code

console.log(a == 1 && a == 0)// true
Copy the code

Can you create A?

Of course it can, as long as a is defined as an object, modify its Symbol. ToPrimitive, toString, valueOf any one can be achieved

Let’s implement each version

let obj = { i: 1}

let objNumber = function(){ return this.i++ }

// Method 1: modify its valueOf method
obj.valueOf = objNumber

// Method 2: Modify toString
// However, as mentioned above, this is not semantic, so we should avoid returning non-string values in toString
obj.toString = objNumber

// Method 3: modify a[Symbol. ToPrimitive]
// However, note that toPrimitive will not automatically call toString and valueOf after adding a[symbol.toprimitive] method
obj[Symbol.toPrimitive] = objNumber

// Of course you can do different things with the parameters passed in to the a[symbol.toprimitive] method, as follows
obj[Symbol.toPrimitive] = function(hint){
    if(hint === 'string') return `value is The ${this.i}`
    return this.i++
}
console.log(String(a)) // "value is 2"

console.log(Number(a)) / / 2
Copy the code

conclusion

ToPrimitive(obj) is called when the == operation is performed on an object type with a string or number, and the ToPrimitive method attempts to call obj[symbol.toprimitive]. Then try calling the OrdinaryToPrimitive method

The OrdinaryToPrimitive method attempts to call toString and valueOf, depending on the OrdinaryToPrimitive arguments.

We can control the result of an object’s conversion by modifying its symbol. toPrimitive, toString, valueOf methods