Implicit type conversion in JavaScript. How much will it be! + [] + [] +! []?

Original author: Alexey Samoshkin
  • Transfer
Type conversion is the process of converting values ​​from one type to another (for example, a string into a number, an object into a logical value, 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 are JS Number, String, Boolean, Null, Undefined. Type added to this list in ES6Symbol, which behaves quite differently from 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 it strange or illogical, although, of course, if you look at the standards, it becomes clear that all these "oddities" are features of the language. Be that as it may, any JS developer periodically has to deal with implicit type conversion, and besides, tricky questions about type conversion may well come up at the interview.

image

This article focuses on the features of type casting mechanisms in JavaScript. We begin it with a list of expressions whose calculation results may look completely unexpected. You can test yourself by trying to find the meanings 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 things that look more than strange, but they work without problems in JS, using implicit type conversion. In the vast majority of cases, implicit type conversion in JS is best avoided. View this list as an exercise to test your knowledge of how typecasting works in JavaScript. If there is nothing new for you here, take a look at wtfjs.com .


JavaScript is full of weirdness.

Here is a page with a table that shows the behavior of the lax equality operator in JavaScript ==, when comparing values ​​of different types. The implicit type conversion performed by the operator ==makes this table much less understandable and logical than, say, a table for the strict equality operator,===The link to which can be found on the above page. It is ==almost impossible to memorize a comparison table for an operator . But you don’t need to remember all this - just 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 accordingly in code, say, in the form Number(value), this is called explicit type conversion (or explicit type conversion).

Since JavaScript is a weakly typed language, values ​​can be converted between different types automatically. This is called implicit type conversion. Usually this happens when the expression is used in the values of different types, such as 1 == null, 2/’5', null + new Date(). Implicit type conversion can also be caused by the context of an expression, such if (value) {…}as where it is valueimplicitly cast to a logical data type.

There is an operator that does not cause an implicit type conversion - is the operator of strict equality ===. 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 conversions:

  • To string ( String)
  • In boolean value ( Boolean)
  • In number ( Number)

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

Primitive Data Types


▍ Converting to String


In order to explicitly convert a value to a string, you can use the function String(). 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 a type, Symbolthings get a little more complicated, since values ​​of this type can be converted to a string type only explicitly. Here you can read more about the conversion rules of type Symbol.

String(Symbol('my symbol'))   // 'Symbol(my symbol)'
'' + Symbol('my symbol')      // ошибка TypeError

▍ Convert to Boolean type


In order to explicitly convert the value to a logical type, use the function Boolean(). Implicit conversion occurs in a logical context, or is invoked by logical operators ( ||&&!).

Boolean(2)          // явное преобразование
if (2) { ... }      // неявное преобразование в логическом контексте
!!2                 // неявное преобразование логическим оператором
2 || 'hello'        // неявное преобразование логическим оператором

Note that operators, like ||, &&perform conversion of values ​​to a logical type for internal purposes, and return the values ​​of the original operands, even if they are not logical.

// это выражение возвращает число 123, а не true
// 'hello' и 123 неявно преобразуются к логическому типу при работе оператора && для вычисления значения выражения
let x = 'hello' && 123;   // x === 123

Since when converting a value to a logical type, only two results are possible - trueor false, it is easiest to master this type of transformation, remembering those expressions that produce false:

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

Any value not included in this list is converted to true, including objects, functions, arrays, dates, as well as user-defined types. Type values ​​are Symbolalso 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

▍ Converting to Number


An explicit conversion to a numeric type is performed using a function Number()— that is, by the same principle as that used for Booleanand types String.

The implicit conversion of a value to a numeric type is a more complicated topic, since it is used, perhaps more often, than conversion to a string or a logical value. Namely, the conversion to type is Numberperformed by the following operators:

  • Comparison Operators ( >, <, <=, >=).
  • Bitwise Operators ( |, &, ^, ~).
  • Arithmetic operators ( -, +, *, /, %). Note that an operator +with two operands does not cause an implicit conversion to a numeric type if at least one operator is a string.
  • Unary operator +.
  • Non-strict equality operator ==(as well !=). Note that the operator ==does not implicitly convert to a number if both operands are strings.

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 trims spaces, as well as the \nand characters \tat the beginning or end of the string, and returns NaNif the resulting string is not a real number. If the string is empty, it is returned 0.

Values nulland are undefinedprocessed differently: nullconverted to 0, while undefinedconverted to NaN.

Type values Symbolcannot be converted to a number either explicitly or implicitly. Moreover, when attempting such a conversion, an error is thrown TypeError. One would expect that this would cause the conversion of the type value Symbolto NaN, as is the case withundefinedbut this does not happen. Details about the rules for converting type values Symbolcan be found on MDN .

Number(Symbol('my symbol'))    // Ошибка TypeError
+Symbol('123')                 // Ошибка TypeError

Here are two special rules to remember:

When applying the operator ==to nullor undefinedconverting to a number is not performed. The value nullis equal to only nullor undefinedand not equal to anything else.

null == 0               // false, null не преобразуется в 0
null == null            // true
undefined == undefined  // true
null == undefined       // true

Meaning is NaNnot equal to anything, including yourself. In the following example, if the value is not equal to itself, then we are dealing withNaN

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

Type conversion for objects


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

The simplest is conversion to a boolean 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 an internal method [[ToPrimitive]]that is responsible for converting to a numerical type and converting to a string.

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

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]]input value and the preferred type to which it must be converted are passed to the method : Numberor String. In this case, the argument is preferredTypeoptional.

Both when converting to a number, and when converting to a string, two methods of the object passed are used [[ToPrimitive]]: this valueOfand toString. Both methods are declared in Object.prototype, and, thus, available for any kind based on Object, for example - it is Date, Arrayand 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 a 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()give a primitive value - give an error TypeError.

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 method valueOf, or have valueOf, which returns the object itself for which it is called ( this), so this value is ignored, since it is not a primitive. That is why converting to a number and to a string can work the same way - both come down to a call toString().

Different operators can either convert to a number or convert to a string using the parameterpreferredType. But there are two exceptions: the non-strict equality ==operator and the operator +with two operands cause a default conversion ( preferredTypenot specified or set to a value default). In this case, most of the built-in types are considered, as a standard variant of behavior, conversion to a number, except for the type Datethat performs the conversion of the object to a string.

Here is an example of Datetype conversion behavior :

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 overridden in order to intervene in the logic of transforming 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

Pay attention to what obj + ‘’returns ‘101’as a string. The operator +calls the standard conversion mode. As already mentioned, it Objectconsiders casting to a number as the default conversion, therefore it uses the method first valueOf()and not toString().

Symbol.toPrimitive ES6 Method


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

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

Analysis of examples


Armed with theory, let us return to the expressions given at the beginning of the material. Here are the results of calculating 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


An operator +with two operands causes a conversion to a number for trueand false:

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

▍12 / '6'


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

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

▍ "number" + 15 + 3


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

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

▍15 + 3 + "number"


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

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

▍ [1]> null


The comparison operator >performs a numerical comparison [1]and null:

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

▍ "foo" + + "bar"


A unary operator +has higher priority than a regular operator +. As a result, the expression is +'bar'evaluated first. Unary +causes a string to 'bar'convert to a number. Since the string is not a valid number, the result is NaN. In the second step, the value of the expression is calculated 'foo' + NaN.

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

▍'true '== true and false ==' false '


The operator ==calls the conversion to number, the string is 'true'converted to NaN, the logical value is trueconverted to 1.

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

▍null == ''


An operator ==usually calls a conversion to a number, but this is not the case with a value null. The value nullis only nullor or undefinednothing more.

null == ''
==> false

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


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

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

▍ ['x'] == 'x'  


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

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

▍ [] + null + 1  


The operator +calls a conversion to a number for an empty array []. The object's method is ArrayvalueOf()ignored because 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 is calculated '' + null + 1.

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

▍0 || "0" && {}  


Logical operators ||and &&during operation lead to the logical value of the operand type, but return source operands (which are of a type different from the logical). The value is 0false, and the value is '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 the objects contain the same values) and two arrays are two different objects, the result will be returned false.

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

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


All operands are not primitive values, so the operator +begins with the leftmost one and causes its conversion to a number. The method is valueOffor types Objectand Arraythese objects themselves return, therefore this value is ignored. The method is toString()used as a fallback. The trick here is that the first pair of curly braces is {}not considered an object literal, it is perceived as a block of code that is ignored. The calculation starts with the next expression, +[]which is converted to an empty string through the method toString(), and then to 0.

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

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


This example is best explained step by step in accordance with the order of operations.

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

▍new Date (0) - 0


The operator -calls a conversion to a number for an object of type Date. The method Date.valueOf()returns the number of milliseconds since the beginning of the Unix era.

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

▍new Date (0) + 0


The operator +invokes the default transformation. Type objects Dataconsider conversion to string to be such a conversion; as a result, the method is used toString(), and not 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'

Summary


Type conversion is one of the basic JavaScript mechanisms, knowledge of which is the basis of productive work. We hope that today's material helped those who were not very well versed in implicit type conversion, put everything in its place, and for those who confidently, without first peeping anywhere, were able to solve the "introductory task", let me recall some interesting case from their practice.

Dear readers! And in your practice, it happened that confusion with implicit type conversion in JavaScript leads to mysterious errors?


Also popular now: