📜 ⬆️ ⬇️

Implicit type conversion in javascript. How many will be! + [] + [] +! []?

Type casting is the process of converting values ​​from one type to another (for example, strings to numbers, objects to logical values, and so on). Any type in JavaScript, whether it is a primitive type, or an object, can be converted to another type. Recall that primitive data types in JS are Number , String , Boolean , Null , Undefined . The Symbol type has been added to this list in ES6, which behaves not at all like other types. Explicit type conversion is a simple and straightforward process, but everything changes when it comes to implicit type conversion. Here, what happens in JavaScript, some consider strange or illogical, although, of course, if you look into the standards, it becomes clear that all these "oddities" are features of the language. Anyway, any JS-developer periodically has to deal with implicit type conversion, besides, tricky questions about type casting may well meet at the interview.

image

This article is devoted to the features of the operation of the casting mechanisms in JavaScript. We will start it with a list of expressions, the results of the calculation of which may look completely unexpected. You can test yourself by trying to find the values ​​of these expressions without looking at the end of the article, where their analysis will be given.

check yourself


Here is a list of interesting expressions that we just talked about:
')
 true + false 12 / "6" "number" + 15 + 3 15 + 3 + "number" [1] > null "foo" + + "bar" 'true' == true false == 'false' null == '' !!"false" == !!"true" ['x'] == 'x' [] + null + 1 0 || "0" && {} [1,2,3] == [1,2,3] {}+[]+{}+[1] !+[]+[]+![] new Date(0) - 0 new Date(0) + 0 

It is full of such that it looks more than strange, but it works without problems in JS, using implicit type conversion. In the vast majority of cases, implicit type conversions in JS are best avoided. Consider this list as an exercise to test your knowledge of how type casting works in JavaScript. If there is nothing new for you, take a look at wtfjs.com .


JavaScript is full of weirdness

Here is a page with a table that shows the behavior of the non-strict equality operator in JavaScript, == , when comparing values ​​of different types. The implicit type conversion performed by the == operator makes this table much less comprehensible and logical than, say, a table for the strict equality operator, === , a link to which can be found on the above page. To memorize the comparison table for the operator == almost impossible. But remembering all this is not necessary - it is enough to master the principles of type conversion used in JavaScript.

Implicit type conversion and explicit type conversion


Type conversion can be explicit and implicit. When a developer expresses an intention to convert a value of one type into a value of another type, writing it appropriately in the code, say, as Number(value) , this is called explicit type conversion (or explicit type conversion).

Since JavaScript is a language with weak typing, values ​​can be converted between different types automatically. This is called implicit type conversion. This usually happens when different values ​​are used in expressions, like 1 == null , 2/'5' , null + new Date() . An implicit type conversion can also be triggered by an expression context, such as if (value) {…} , where value implicitly cast to a logical data type.

There is an operator that causes no implicit type conversion - this is a strict equality operator, === . The non-strict equality operator, == , on the other hand, performs the comparison operation and, if necessary, performs implicit type conversion.

Implicit type conversion is a double-edged sword: it is a source of confusion and errors, but it is also a useful mechanism that allows you to write less code without losing its readability.

Three types of type conversion


The first feature of working with types in JS that you need to know about is that there are only three types of transformations:


The second feature of JS that needs to be considered is that the conversion logic for primitive types and for objects works differently, but primitives and objects can be converted into these three types. Let's start with primitive data types.

Primitive data types


▍ Conversion to String


In order to explicitly convert a value to a string, you can use the String() function. An implicit conversion causes the use of the usual addition operator, + , with two operands, if one of them is a string:

 String(123) //   123 + ''    //   

All primitive types are converted to strings in a completely natural and expected way:

 String(123)                   // '123' String(-12.3)                 // '-12.3' String(null)                  // 'null' String(undefined)             // 'undefined' String(true)                  // 'true' String(false)                 // 'false' 

In the case of the Symbol type, the case is somewhat more complicated, since values ​​of this type can be converted to a string type only explicitly. Here you can read the details about the conversion rules of type Symbol.

 String(Symbol('my symbol'))   // 'Symbol(my symbol)' '' + Symbol('my symbol')      //  TypeError 

▍ Conversion to Boolean


To explicitly convert a value to a boolean type, use the Boolean() function. An implicit conversion occurs in a logical context, or is called by logical operators ( || && ! ).

 Boolean(2)          //   if (2) { ... }      //      !!2                 //     2 || 'hello'        //     

Notice that operators like || and && convert values ​​to a logical type for internal purposes, and return the values ​​of the source operands, even if they are not logical.

 //     123,   true // 'hello'  123         &&     let x = 'hello' && 123;   // x === 123 

Since when casting a value to a logical type, only two results are possible - true or false , it is easiest to master this kind of conversion, remembering the expressions that return false :

 Boolean('')           // false Boolean(0)            // false    Boolean(-0)           // false Boolean(NaN)          // false Boolean(null)         // false Boolean(undefined)    // false Boolean(false)        // false 

Any value not in this list is converted to true , including objects, functions, arrays, dates, and user-defined types. Values ​​of type Symbol also converted to true . Empty objects and empty arrays are also converted to true :

 Boolean({})             // true Boolean([])             // true Boolean(Symbol())       // true !!Symbol()              // true Boolean(function() {})  // true 

▍ Conversion to Type Number


Explicit conversion to a numeric type is performed using the Number() function - that is, according to the same principle used for the Boolean and String types.

Implicit coercion of a value to a numeric type is a more complicated topic, since it is applied, perhaps, more often than conversion to a string or a logical value. Namely, the conversion to the Number type is performed by the following operators:


 Number('123')   //   +'123'          //   123 != '456'    //   4 > '5'         //   5/null          //   true | 0        //   

Here’s how primitive values ​​are converted to numbers:

 Number(null)                   // 0 Number(undefined)              // NaN Number(true)                   // 1 Number(false)                  // 0 Number(" 12 ")                 // 12 Number("-12.34")               // -12.34 Number("\n")                   // 0 Number(" 12s ")                // NaN Number(123)                    // 123 

When converting strings to numbers, the system first truncates spaces, as well as the \n and \t characters at the beginning or end of the string, and returns NaN if the resulting string is not a real number. If the string is empty, 0 is returned.

Values ​​of null and undefined handled differently: null converted to 0 , while undefined converted to NaN .

Values ​​of type Symbol cannot be converted to a number, either explicitly or implicitly. Moreover, when attempting such a conversion, a TypeError error is TypeError . One would expect that this would cause the conversion of a Symbol value to NaN , as it does with undefined , but it does not. Details on the rules for converting Symbol type values ​​can be found at MDN .

 Number(Symbol('my symbol'))    //  TypeError +Symbol('123')                 //  TypeError 

Here are two special rules to remember:

When applying the == operator to null or undefined conversion to a number is performed. The value null is only null or undefined and is not equal to anything else.

 null == 0               // false, null    0 null == null            // true undefined == undefined  // true null == undefined       // true 

The value of NaN not equal to anything, including yourself. In the following example, if the value is not equal to itself, then we are dealing with NaN

 if (value !== value) { console.log("we're dealing with NaN here") } 

Type conversion for objects


So, we have considered type conversion for primitive values. It's all pretty simple. When it comes to objects, and the system encounters expressions like [1] + [2,3] , it first needs to convert the object to a primitive value, which is then converted to its final type. When working with objects, we recall, there are also only three directions of transformations: to a number, to a string, and to a logical value.

The simplest is to convert to a logical value: any value that is not a primitive is always implicitly converted to true , this is also true for empty objects and arrays.

Objects are converted to primitive values ​​using the [[ToPrimitive]] internal method, which is responsible for both the conversion to a numeric type and the conversion to a string.

Here is the pseudo-implementation of the [[ToPrimitive]] method:

 function ToPrimitive(input, preferredType){ switch (preferredType){   case Number:     return toNumber(input);     break;   case String:     return toString(input);     break   default:     return toNumber(input);  } function isPrimitive(value){   return value !== Object(value); } function toString(){   if (isPrimitive(input.toString())) return input.toString();   if (isPrimitive(input.valueOf())) return input.valueOf();   throw new TypeError(); } function toNumber(){   if (isPrimitive(input.valueOf())) return input.valueOf();   if (isPrimitive(input.toString())) return input.toString();   throw new TypeError(); } } 

The [[ToPrimitive]] passed an input value and the preferred type to convert it to: Number or String . The preferredType argument is optional.

Both when converting to a number and converting to a string, two methods of the object passed to [[ToPrimitive]] : this is valueOf and toString . Both methods are declared in the Object.prototype , and are thus available for any type based on Object , for example - Date , Array , and so on.

In general, the operation of the algorithm is as follows:

  1. If the input value is a primitive, do nothing and return it.
  2. Call input.toString() , if the result is a value of the primitive type - return it.
  3. Call input.valueOf() , if the result is a value of a primitive type - return it.
  4. If neither input.toString() nor input.valueOf() gives a primitive value, give a TypeError error.

When converting to a number, valueOf (3) is first called, if the result cannot be obtained, toString (2) is called. When converting to a string, the reverse sequence of actions is used — first toString (2) is called, and in case of failure, valueOf (3) is called.

Most built-in types do not have a valueOf method, or have a valueOf , which is returned by the object for which it is called ( this ), so this value is ignored, since it is not a primitive. That is why converting to numbers and strings can work the same way - both come down to the toString() call.

Different operators can call either a conversion to a number or a conversion to a string using the preferredType parameter. But there are two exceptions: the lax equality operator == and the + operator with two operands cause a default conversion (the preferredType not specified or is set to default ). In this case, most of the built-in types are considered, as a standard behavior, conversion to a number, with the exception of the Date type, which converts an object into a string.

Here is an example of Date behavior on type conversion:

 let d = new Date(); //    let str = d.toString();  // 'Wed Jan 17 2018 16:15:42' //   ,   -      Unix let num = d.valueOf();   // 1516198542525 //     //  true   d      console.log(d == str);   // true //     //  false,   d       valueOf() console.log(d == num);   // false //  'Wed Jan 17 2018 16:15:42Wed Jan 17 2018 16:15:42' // '+',  ,   '==',      console.log(d + d); //  0,    '-'     ,      console.log(d - d); 

Standard methods toString() and valueOf() can be redefined to intervene in the logic of converting an object into primitive values.

 var obj = { prop: 101, toString(){   return 'Prop: ' + this.prop; }, valueOf() {   return this.prop; } }; console.log(String(obj));  // 'Prop: 101' console.log(obj + '')      // '101' console.log(+obj);         //  101 console.log(obj > 100);    //  true 

Notice that obj + '' returns '101' as a string. The + operator calls the standard transform mode. As already mentioned, Object treats coercion to a number as a default conversion, so it uses the valueOf() method first and not the toString() method.

Method Symbol.toPrimitive ES6


In ES5, it is permissible to change the logic of converting an object to a primitive value by overriding the toString and valueOf methods.

In ES6, you can go even further and completely replace the internal mechanism [[ToPrimitive]] by implementing the object method [Symbol.toPrimtive] .

 class Disk { constructor(capacity){   this.capacity = capacity; } [Symbol.toPrimitive](hint){   switch (hint) {     case 'string':       return 'Capacity: ' + this.capacity + ' bytes';     case 'number':       //   KiB       return this.capacity / 1024;     default:       //            return this.capacity / 1024;   } } } // 1MiB  let disk = new Disk(1024 * 1024); console.log(String(disk))  // Capacity: 1048576 bytes console.log(disk + '')     // '1024' console.log(+disk);        // 1024 console.log(disk > 1000);  // true 

Case study


Armed with theory, let us return to the expressions given at the beginning of the material. Here are the results of evaluating these expressions:

 true + false             // 1 12 / "6"                 // 2 "number" + 15 + 3        // 'number153' 15 + 3 + "number"        // '18number' [1] > null               // true "foo" + + "bar"          // 'fooNaN' 'true' == true           // false false == 'false'         // false null == ''               // false !!"false" == !!"true"    // true ['x'] == 'x'             // true [] + null + 1            // 'null1' 0 || "0" && {}           // {} [1,2,3] == [1,2,3]       // false {}+[]+{}+[1]             // '0[object Object]1' !+[]+[]+![]              // 'truefalse' new Date(0) - 0          // 0 new Date(0) + 0          // 'Thu Jan 01 1970 02:00:00(EET)0' 

Let us examine each of these examples.

▍true + false


The + operator with two operands causes a conversion to a number for true and false :

 true + false ==> 1 + 0 ==> 1 

▍12 / '6'


The arithmetic division operator, / , causes a conversion to a number for the string '6' :

 12 / '6' ==> 12 / 6 ==>> 2 

▍ “number” + 15 + 3


The + operator has left-to-right associativity, so the expression "number" + 15 is executed first. Since one of the operands is a string, the + operator causes a conversion to the string for the number 15 . In the second step, the evaluation of the expression "number15" + 3 processed in the same way:

 "number" + 15 + 3 ==> "number15" + 3 ==> "number153" 

▍15 + 3 + “number”


The 15 + 3 expression is evaluated first. There is absolutely no need for type conversion, since both operands are numbers. In the second step, the value of the expression 18 + 'number' calculated, and since one of the operands is a string, a conversion to the string is called.

 15 + 3 + "number" ==> 18 + "number" ==> "18number" 

▍ [1]> null


The comparison operator > performs numeric comparison [1] and null :

 [1] > null ==> '1' > 0 ==> 1 > 0 ==> true 

▍ "foo" + + "bar"


The unary + operator has a higher priority than the normal + operator. As a result, the expression +'bar' evaluated first. Unary + invokes a conversion to a number for the string 'bar' . Since the string is not a valid number, the result is NaN . In the second step, the value of the expression 'foo' + NaN calculated.

 "foo" + + "bar" ==> "foo" + (+"bar") ==> "foo" + NaN ==> "fooNaN" 

Ru'true '== true and false ==' false '


The == operator causes the conversion to a number, the string 'true' converted to NaN , the Boolean value true converted to 1 .

 'true' == true ==> NaN == 1 ==> false false == 'false'  ==> 0 == NaN ==> false 

▍null == ''


The == operator usually converts to a number, but this is not the case with the value null . The value null is only null or undefined and nothing else.

 null == '' ==> false 

▍ !! "false" == !! "true"


Operator !! converts the strings 'true' and 'false' to boolean true , since they are non-empty strings. Then the == operator simply checks the equality of the two logical values true without type conversion.

 !!"false" == !!"true" ==> true == true ==> true 

▍ ['x'] == 'x'


The == operator calls a conversion to a numeric type for arrays. The object method Array.valueOf() returns the array itself, and this value is ignored because it is not a primitive. The array method toString() converts the array ['x'] into the string 'x' .

 ['x'] == 'x' ==> 'x' == 'x' ==>  true 

▍ [] + null + 1


The + operator causes conversion to a number for the empty array [] . The method of the Array object valueOf() ignored, since it returns the array itself, which is not a primitive. The array method toString() returns an empty string.

In the second step, the value of the expression '' + null + 1 calculated.

 [] + null + 1 ==>  '' + null + 1 ==>  'null' + 1 ==> 'null1' 

▍0 || "0" && {}


Logical Operators || and && in the course of operation, cast the operands to a logical type, but return the source operands (which are of a different type than the logical one). The value 0 false, and the value '0' true, since it is a non-empty string. An empty object {} is also converted to a true value.

 0 || "0" && {} ==>  (0 || "0") && {} ==> (false || true) && true  //   ==> "0" && {} ==> true && true             //   ==> {} 

▍ [1,2,3] == [1,2,3]


Type conversion is not required since both operands are of the same type. Since the == operator checks for equality of references to objects (and not whether objects contain the same values) and two arrays are two different objects, false will be returned as a result.

 [1,2,3] == [1,2,3] ==>  false 

▍ {} + [] + {} + [1]


, + . valueOf Object Array , . toString() . , {} , , . , +[] , toString() , 0.

 {}+[]+{}+[1] ==> +[]+{}+[1] ==> 0 + {} + [1] ==> 0 + '[object Object]' + [1] ==> '0[object Object]' + [1] ==> '0[object Object]' + '1' ==> '0[object Object]1' 

▍!+[]+[]+![]


.

 !+[]+[]+![] ==> (!+[]) + [] + (![]) ==> !0 + [] + false ==> true + [] + false ==> true + '' + false ==> 'truefalse' 

▍new Date(0) — 0


- Date . Date.valueOf() Unix.

 new Date(0) - 0 ==> 0 - 0 ==> 0 

▍new Date(0) + 0


+ . Data , toString() , valueOf() .

 new Date(0) + 0 ==> 'Thu Jan 01 1970 02:00:00 GMT+0200 (EET)' + 0 ==> 'Thu Jan 01 1970 02:00:00 GMT+0200 (EET)0' 

Results


— JavaScript, . , , , , , , , , « », - .

Dear readers! , JavaScript ?

Source: https://habr.com/ru/post/347866/


All Articles