preface

Casting in JavaScript has always been one of the biggest headaches for front-end developers. A while back, a guy on Twitter posted a graphic saying that JavaScript is amazing.

In addition to this, there are many classic type conversions that confuse JavaScript developers, such as the following. Do you know what the results are?

1+ {} = = =? {} +1= = =?1+ [] = = =?1 + '2'= = =?Copy the code

This article will take you from the ECMA specification to a deeper understanding of conversions in JavaScript so that conversions are no longer a front-end development impediment.

The data type

JS has six simple data types: undefined, null, Boolean, string, number, symbol, and a complex type: object. But JavaScript has only one type at declaration time, and the current type is determined only at runtime. At run time, because JavaScript does not place strict restrictions on types, different types can be evaluated, so you need to allow conversions between types.

Type conversion

Explicit type conversion

Explicit type conversion is the manual conversion of one value to another. In general, explicit conversions are done exactly as shown in the table above.

Common explicit type conversion methods are Number, String, Boolean, parseInt, parseFloat, toString, and so on. One thing to notice here about parseInt is that there’s a question that you occasionally get in an interview.

Q: Why does [1, 2, 3].map(parseInt) return [1,NaN,NaN]? Answer: The second argument to parseInt represents the cardinality of the number to be parsed. The value ranges from 2 to 36.

If this parameter is omitted or its value is 0, the number is parsed on the basis of 10. If it starts with “0x” or “0x”, base 16 will be used.

If the parameter is less than 2 or greater than 36, parseInt() will return NaN.

Generally speaking, type conversion mainly consists of basic type to basic type and complex type to basic type. The target types of the conversion are divided into the following types:

  1. convertstring
  2. convertnumber
  3. convertboolean

I refer to the official ecMA-262 documentation to summarize these types of conversions. ECMA documentation link: ECMA-262

ToNumber

Other rules for converting types to number are shown in the table below:

The original value Conversion results
Undefined NaN
Null 0
true 1
false 0
String Transform according to syntax and transformation rules
Symbol Throw a TypeError exception
Object First call toPrimitive, then call toNumber
StringconvertNumberType of rules:
  1. If the string contains only numbers, it is converted to the corresponding number.
  2. If the string contains only hexadecimal format, it is converted to the corresponding decimal number.
  3. If the string is empty, it is converted to 0.
  4. If the string contains characters other than those described above, convert to NaN.

Use + to convert other types to number. Let’s verify this with the following example.

+undefined // NaN
+null / / 0
+true / / 1
+false / / 0
+'111' / / 111
+'0x100F' / / 4111
+' ' / / 0
'b' + 'a' + + 'a' + 'a' // 'baNaNa'
+Symbol(a)// Uncaught TypeError: Cannot convert a Symbol value to a number
Copy the code

ToBoolean

The original value Conversion results
Undefined false
Boolean true or false
Number 0 and NaN return false, others return true
Symbol true
Object true
We can also useBooleanConstructor to manually convert other typesbooleanType.
Boolean(undefined) // false
Boolean(1) // true
Boolean(0) // false
Boolean(NaN) // false
Boolean(Symbol()) // true
Boolean({}) // true
Copy the code

ToString

The original value Conversion results
Undefined ‘Undefined’
Boolean ‘true’ or ‘false’
Number The value is a string type
String String
Symbol Throw a TypeError exception
Object First call toPrimitive, then call toNumber
The switch tostringTypes can be implemented using template strings.
`The ${undefined}` // 'undefined'
`The ${true}` // 'true'
`The ${false}` // 'false'
`The ${11}` / / '11'
`The ${Symbol()}` // Cannot convert a Symbol value to a string
`The ${{}}`
Copy the code

Implicit type conversion

Implicit conversions typically occur when operators are involved, such as when we add two variables, or when we compare whether two variables are equal. Implicit conversions have been shown in our example above. ToPrimitive’s rules are also followed for converting objects ToPrimitive types, as described below.

Casting from the ES specification

ToPrimitive

When an object is converted to its original type, the built-in ToPrimitive method is typically called, which in turn calls the OrdinaryToPrimitive method. Take a look at the official ECMA documentation.

Let me translate this passage.

The ToPrimitive method takes two arguments, the input value, and the desired conversion type, PreferredType.

  1. If it’s not passed inPreferredTypeParameters,hintIs equal to “default”
  2. ifPreferredTypehint StringAnd lethintIs equal to “string”
  3. ifPreferredTypehint NumberAnd lethintIs equal to the “number”
  4. letexoticToPrimIs equal to theGetMethod(input, @@toPrimitive)Get the parameterinput@@toPrimitivemethods
  5. ifexoticToPrimnotUndefined, then letresultIs equal to theCall(exoticToPrim, input, « hint »)“, which means to executeexoticToPrim(hint), if the result of the executionresultIs the raw data type and returnsresultOtherwise, an exception of the wrong type is thrown
  6. ifhintBe “default”, let mehintIs equal to the “number”
  7. returnOrdinaryToPrimitive(input, hint)Abstract the result of the operation

OrdinaryToPrimitive

The OrdinaryToPrimitive method also takes two arguments, the input value O and a hint, which is also the type of the expected conversion.

  1. If the input value is an object
  2. ifhintIt’s a string and its value is either ‘string’ or ‘number’.
  3. ifhintIs ‘string’, then it’s going tomethodNamesSet totoString,valueOf
  4. ifhintIs ‘number’, then it’s going tomethodNamesSet tovalueOf,toString
  5. traversemethodNamesGets the value in the current loopnameThat will bemethodSet toO[name](that is, to get thevalueOftoStringTwo methods)
  6. ifmethodCan be called, so let’sresultIs equal to themethodThe result after execution, ifresultReturn if it is not an objectresultOtherwise, an error of type is thrown.

ToPrimitive code implementation

If you just use words to describe it, you will find it too difficult to understand, so I will implement the two methods in code to help you understand.

// Get the type
const getType = (obj) = > {
    return Object.prototype.toString.call(obj).slice(8, -1);
}
// Whether it is a primitive type
const isPrimitive = (obj) = > {
    const types = ['String'.'Undefined'.'Null'.'Boolean'.'Number'];
      returntypes.indexOf(getType(obj)) ! = = -1;
}
const ToPrimitive = (input, preferredType) = > {
    // If input is a primitive type, then no conversion is required
    if (isPrimitive(input)) {
        return input;
    }
    let hint = ' ', 
        exoticToPrim = null,
        methodNames = [];
    // Hint defaults to "default" when the optional parameter preferredType is not provided;
    if(! preferredType) { hint ='default'
    } else if (preferredType === 'string') {
        hint = 'string'
    } else if (preferredType === 'number') {
        hint = 'number'
    }
    exoticToPrim = input.@@toPrimitive;
    // If there is a toPrimitive method
    if (exoticToPrim) {
        // If an exoticToPrim execution returns an original type
        if (typeof(result = exoticToPrim.call(O, hint)) ! = ='object') {
            return result;
        // If an exoticToPrim execution returns an object type
        } else {
            throw new TypeError('TypeError exception')}}// Give the default hint value number, Symbol and Date by specifying @@toprimitive
    if (hint === 'default') {
        hint = 'number'
    }
    return OrdinaryToPrimitive(input, hint)
}
const OrdinaryToPrimitive = (O, hint) = > {
    let methodNames = null,
        result = null;
    if (typeofO ! = ='object') {
        return;
    }
    // This determines whether toString or valueOf is called first
    if (hint === 'string') {
        methodNames = [input.toString, input.valueOf]
    } else {
        methodNames = [input.valueOf, input.toString]
    }
    for (let name in methodNames) {
        if (O[name]) {
            result = O[name]()
            if (typeofresult ! = ='object') {
                return result
            }
        }
    }
    throw new TypeError('TypeError exception')}Copy the code

To summarize, when casting, the ToPrimitive method is used to convert the reference type to the original type. If @@toprimitive has a method on the reference type, the @@toprimitive method is called, and the value returned after execution is the original type. If it is still an object, then an error is thrown.

If there is no toPrimitive method on the object, the toString or valueOf method is called first based on the target type of the conversion, and if both methods result in a valueOf the original type, the value is returned. Otherwise, an error will be thrown.

Symbol.toPrimitive

The Symbol. ToPrimitive method is provided after ES6, which has the highest priority when casting.

const obj = {
  toString() {
    return '1111'
  },
  valueOf() {
    return 222},Symbol.toPrimitive]() {
    return Awesome!}}const num = 1 + obj; / / 667
const str = '1' + obj; / / '1666'
Copy the code

example

In case the ToPrimitive code is too confusing for you, let me give you a few examples of object conversions.

var a = 1, 
    b = '2';
var c = a + b; / / '12'
Copy the code

You might wonder, why not convert the b to number and get 3? Once again, we need to look at the document’s definition of the plus sign.

First, the toPrimitive method executes the two values separately, and because a and B are primitive types, you still get 1 and ‘2’. As you can see from the diagram, if either of the converted values has a Type of String, then the two values will be String after the toString conversion. So you end up with ’12’ instead of 3.

We can look at one more example.

var a = 'hello ', b = {};
var c = a + b; // "hello [object Object]"
Copy the code

Here will also perform two values toPrimitive method respectively, and a still received the ‘hello’, and because there is no specified preferredType b, so will be into the default number type, call the valueOf first, but the valueOf returned an empty object, It’s not a primitive type, so we call toString again, and we get ‘[object object]’, and then we concatenate the two and we get “Hello [object object]”. What if we want to return ‘Hello world’? All you need to do is change the valueOf method for b.

b.valueOf = function() {
    return 'world'
}
var c = a + b; // 'hello world'
Copy the code

You may have seen this example in an interview question.

var a = [], b = [];
var c = a + b; / /"
Copy the code

Why does c end up with “prime” here? Because a and B still get a [] after valueOf, which is not the original type, they continue toString execution and get “”, and add the two” “to get” “. Let’s look at another example where we specify a preferredType.

var a = [1.2.3], b = {
    [a]: 111
}
Copy the code

PreferredType = string, preferredType = string, preferredType = string, preferredType = string

The problem summary

It has been posted on other platforms for some time, and some questions raised by others are also posted here by the way.

conclusion

Type conversion has always been a difficult concept to understand when learning JS, because the conversion rules are complicated and often make people feel confused.

But if you understand the principles of these transformation rules from the ECMA specification, it’s easy to see why the results are there.

If you’re interested, you can follow me on Github. Here’s my latest blog: github.com/yinguangyao…