JavaScript: The Surprising Parts
Do you think you know all the surprising parts of JavaScript? Some of these "features" may look as if the language was broken, but it's not necessarily the case. Things like variables hoisting, variables scope, behaviour of this
are quite intentional and besides just being different from most of other programming languages, there is nothing particularly wrong with them. However, there are still some things that are quite surprising about JavaScript. Let's take a look at some of them.
Surprise #1 - parseInt
function
Imagine you have some numbers as strings and you want to convert them to integers. You could probably use Number()
function to do that, but let's assume you are used to parseInt()
function. Let's do some conversions then:
["1"].map(parseInt);
// [1]
["1", 2, "3", 4].map(parseInt);
// => [1, NaN, NaN, NaN] // WUT ?!
["1", 2, "3", 4].map(parseFloat);
// => [1, 2, 3, 4] // WUT...
Something is definitely wrong here. How could possibly parseFloat()
work fine here and parseInt()
not? Obviously JavaScript is broken, right?
Not really. This is actually the expected behaviour. The difference between parseFloat
and parseInt()
is that parseFloat()
takes only one argument (string
), but parseInt()
takes two arguments - string
and... radix
. To verify it, let's rewrite the mapping using an anonymous function:
["1", 2, "3", 4].map((number) => parseInt(number));
// => [1, 2, 3, 4]
When you pass simply parseInt()
function as an argument to map()
, the second argument (which is a current index) is going to be passed as radix
to parseInt
, which explains why it returns NaN
. The equivalent of just passing parseInt
looks like this:
["1", 2, "3", 4].map((number, index, array) => parseInt(number, index, array));
As "odd" as it may look like, this is a perfectly valid behaviour and there is nothing wrong with JavaScript
;).
Surprise #2 - sorting
Now that we've learned how to parse integers in JavaScript like a boss, let's do some sorting:
[1, 20, 2, 100].sort();
// => [1, 100, 2, 20] // WUT again...
Again, something odd is going on here. However, this is the intended behavior - after consulting with docs, we can learn that sort()
converts all elements into strings and compares them in Unicode code point order. I think this might be a big surprise for a majority of developers performing sorting and seeing the result, but this behaviour is clearly documented. Due to the necessity of maintaing backwards compatibility, I wouldn't expect this behavior to change, so it's worth keeping it in mind.
To perform sorting on integers you need to provide a compare function:
[1, 20, 2, 100].sort((a, b) => a - b);
// => [1, 2, 20, 100]
Surprise #3 - ==
vs. ===
You've probably heard that you should never use double equals (loose equality) and just stick to triple equals (strict equaility). But following some rules without understanding the reasons behind them is never a good solution to a problem. Let's try to understand how these operators work.
Loose equality (==) compares two values after converting them to common type. After conversions (both of the values can be converted) the comparison is performed by strict equality (===). So what is a common type in this case?
The easiest way to get the idea what happens when using ==
would be checking the table for conversion rules here as it's not really something obvious (the details of how conversion works are described later in this article).
So basically this table says that the following expressions will be truthy:
1 == "1";
"1" == 1;
new String("string") == "string";
1 == { valueOf: function() { return 1; } }; // lulz, it's not a joke ;)
1 == true;
false == 0;
"Fri Jan 01 2016 00:00:00 GMT+0100 (CET)" == new Date(2016, 0, 1)
"1,2" == [1,2] // because why not?
Seems quite "exotic", right? But is loose equality actually useful?
Yes, it is. I can imagine 3 scenarios where it comes in handy.
The first scenario would be comparing integers from the forms when you don't really care about strict equality and types:
if ($('.my-awesome-input').val() == 100) {
// do something here
}
In such case it may turn out that we don't really care if we compare strings or integers, either "100"
and 100
are fine and we don't need to perform any explicit conversions.
The second use case would be treating both undefined
and null
as the same thing meaning lack of some value. With strict equality we would need to check for both values:
x = getSomeValue();
if (x !== undefined && x !== null) {
// some logic
}
Doesn't look that nice. We could clean it up with loose equality and simply check if something is not null
-ish:
x = getSomeValue();
if (x != null) {
// some logic
}
The last use case would be comparing primitives and objects. It's especially useful when dealing with both primitive strings ("simple string"
) and strings as objects (new String("string as object")
:
x = getSomeValue();
if (x != "some special value") {
// some logic
}
With strict equality we would probably need to explicitly convert objects to strings using toString()
, which is not that bad, but loose equality looks arguably cleaner.
Surprise #4 - equality gotcha #1: NaN
Do you know how to identify NaN in JavaScript. Sounds like a silly question, right? Well, not really, both of the following expressions are falsey:
NaN == NaN; // => false
NaN === NaN; // => false
Fortunately, there is still a way to check for NaN
: it is the only value in JS that is not equal to itself:
NaN != NaN; // => true
NaN !== NaN; // => true
You could either take advantage of this behaviour or use isNaN
function:
isNaN(NaN); // => true
There is one more possibility to test for NaN
: Object.is
function, which is very similar to strict equality, but with few exceptions. One of those is comparing NaN
values:
NaN === NaN // => false
Object.is(NaN, NaN); // => true
Surprise #5 - equality gotcha #2: comparing objects
There is one more gotcha besides NaN
when it comes to testing for equality: comparing objects. If you think you can easily compare arrays with the same elements or objects with the same keys and values, you might be quite surprised:
[1, 2, 3] == [1, 2, 3]; // false
[1, 2, 3] === [1, 2, 3]; // false
{ comparing: "objects" } == { comparing: "objects" }; // false
{ comparing: "objects" } === { comparing: "objects" }; // false
The reason behind it is quite simple though: strict equality doesn't compare the values, but identities instead. And two different objects are, well, different, unless they are referring to the exactly same thing.
How about loose equality? As already discussed, if the types are the same, the values are compared using strict equality. It doesn't work with Object.is
either. The only option for objects is to compare each key and associated value with the ones from the other object.
Surprise #6 - instanceof
and typeof
There seems to be a lot of confusion regarding those two and how use them in different contexts. Basically, typeof
should be used for getting the basic JavaScript type of given expression (i.e. undefined, object, boolean, string, number, string, function or symbol) and instanceof should be used for checking if a prototype of a given constructor is present in expression's prototype chain. Even if they may seem to be similar at times, they should be used in very different use cases, check the following examples:
typeof "basic string" // => "string", it's a primitive so looks good so far
typeof new String("basic string" ) // => "object", because it's no longer a primitive!
"basic string" instanceof String // => false, because "basic string" is a primitive
1 instanceof Number // => false, same reason, 1 is a primitive
[] instanceof Array // => true
[] instanceof Object // => true, array is not a primitive
typeof [] // => "object", there is no array primitive, it's still an object
Unforunately, it's not that easy in all cases. There are 2 exceptions regarding usage of typeof
that are quite surprising.
There is undefined type which would be returned for undefined
expression, but what about null
? Turns out that its type is object! There were some attempts to remove this confusion - like this proposal for introducing null type - but they were eventually rejected.
And another suprise: NaN. What is the type of something that is not a number? Well, it's number of course ;). As funny as it sounds, it is in accordance with IEEE Standard for Floating-Point Arithmetic and the concept of NaN
is kind of number-ish, so this behaviour is somehow justified.
Surprise #7 - Number.toFixed()
returning strings
Imagine you want to round some number in JavaScript and do some math with it. Apparently Math.round()
is capable only of rounding to the nearest integer, so we need to find some better solution. There is Number.toFixed()
function which seems to do the job. Let's try it out:
123.789.toFixed(1) + 2 // => 123.82, huh?
Is math broken in JS? Not really. It's just the fact that Number.toFixed()
returns a string, not a numeric type! And its intention is not really to perform rounding for math operations, it's only for formatting! Too bad there is no built-in function to do such simple operation, but if you expect a numeric type, you can just handle it with +
unary prefix operator, which won't be used as an addition operator, but will perform conversion to number in such case:
const number = +123.789.toFixed(1);
number // => 123.8
Surprise #8 - Plus (+
) operator and results of addition
"Adding stuff in JavaScript is simple, obvious and not surprising" - No one ever
Have you ever watched Wat by Gary Bernhardt? If not, I highly encourage you to do it now, it's absolutely hillarious and concerns a lot of "odd" parts of JavaScript.
Let's try to explain most of those odd results when using +
operator. Beware: once you finishing reading it, you will actually not find most of these results that surprising, it will be just "different". I'm not sure yet if it's a good or a bad thing :).
Take a look at the following examples:
[] + [] // => ""
[] + {} // => "[object Object]"
{} + [] // => 0 // wut...
{} + {} // => "[object Object][object Object]"
[] + 1 // => "1" // it's a string!
[1] + 2 => // => "12"
[1, 2] + 2 // => "1,22"
3 + true // => 4
new Date(2016, 0 , 1) + 123 // => "Fri Jan 01 2016 00:00:00 GMT+0100 (CET)123"
({ toString: function() { return "trolololo"; } }) + new Date(2016, 0 , 1) // => "trolololoFri Jan 01 2016 00:00:00 GMT+0100 (CET)"
1 + { valueOf: function() { return 10; }, toString: function() { return 5; } } // => 11
1 + { toString: function() { return 10; } } // => 11
1 + undefined // => NaN
1 + null // => 1
All of these results may seem to be somehow exotic, but only one of them, maybe two at most, are exceptional. The basic thing before figuring out the result of those expressions is understanding what is happening under the hood. In JavaScript you can only add numbers and strings, all other types must be converted to one of thos before. The +
operator basically converts each value to primitive (which are: undefined, null, booleans, numbers and strings). This convertion is handled by the internal operation called ToPrimitive
which has the following signature: ToPrimitive(input, PreferredType)
. The PreferredType
can be either number or string. The algorithm of this operation is quite simple, here are the steps if string is the preferred type:
- return
input
if it's already a primitive - If it's not a primitive, call
toString()
method oninput
and return the result if it's a primitive value - If it's not a primitive, call
valueOf()
method oninput
and return the result if it's a primitive value - If it's not a primitive, throw
TypeError
For number
as preferred type the only difference is the sequence of steps 2 and 3: valueOf
method will be called first and if it doesn't return a primitive then toString
method wil be called. In most cases number
will be the preferred type, string
will be used only when dealing with the instances of Date
.
Now that we know what is going on under the hood let's explain the results from the examples above.
The result of calling valueOf
method on objects ({}
) and arrays (which technically are also objects) is simply the object itself, so it's not a primitive. However, for objects toString()
method will return "[object Object]"
and for arrays it will return empty string - ""
. Now we have primitives that can be added. From this point we can predict the results of operation like {} + {}
, [] + {}
or even:
1 + { valueOf: function() { return 10; }, toString: function() { return 5; } };
and:
1 + { toString: function() { return 10; } };
If you remember that ({ toString: function() { return "surprise!"; } }) + new Date(2016, 0 , 1)
is not really surprising anymore. But how is it possible that {} + []
returns 0
, not "[object Object]"
?
Most likely {}
in the beginning is interpreted as an empty block and it's ignored. You can verify it by putting the empty object inside parentheses (({}) + []
), the result will be "[object Object]"
. So in fact that expression is interpreted as +[]
which is very different from the addition! As I've already mentioned before, it's the unary prefix operator which performs conversion to number. For arrays the result of such conversion is simply 0.
And why does 1 + undefined
return NaN
? We can add only numbers and strings, undefined
is neither of them, so it must be converted to a number in this case. The result of such operation is simply NaN
and 1 + NaN
is still NaN
.
Surprise #9 - No integers and floats - just numbers
In most programming languages there are different type of numbers, like integers and floats. What is again surprising about JavaScript is that all numbers are simply double precision floating point numbers! This has a huge impact of anything related to math, even for such things like precision. Take a look at the following example:
9999999999999999 === 10000000000000000 // => true
This is definitely not something that would be expected here. If you are planning to do any serious math in JavaScript, make sure you won't run into any issues caused by the implementation of the numbers.
Wrapping up
JavaScript may sometimes seem like it's "broken" somehow, especially comparing to other programming languages. However, many of these features are quite intentional and others are consequences of some decisions. There are still few things that seem to be really odd, but after digging deeper they start to make sense (or at least don't look like some voodoo magic), so to avoid unfortunate surprises it's definitely worth learning about those odd parts of JavaScript.