The WATs of JavaScript

Published Monday, June 29 2015

At CodeMash 2012, Gary Bernhardt gave a now infamous lightning talk that has become known simply as The WAT Talk, in which he presents several of the more surprising behaviors of Ruby and JavaScript. I’ve passed the video around quite a few times, and I’ve pointed out some other JavaScript behaviors that seem pretty outlandish at first sight. But I’m feeling a little guilty about poking fun at JavaScript, so I wanted to dive further into these WATs and talk about why they happen.

I’m not here to defend JavaScript—it doesn’t need my defense. It’s not a perfect language, and it’s not my favorite language, but it is the single most popular language on the web right now, and because more and more people are using it for the first time, I think it’s worth the effort to go over some of these unexpected behaviors to help newcomers avoid common pitfalls.

But first, the WATs.

The WATs

> [] == []
false
> [] == ![]
true
> [] + []
''
> [] - []
0
> [ null, undefined, [] ] == ',,'
true
> [] + {}
'[object Object]'
> {} + []
0
> Math.min() < Math.max()
false
> 10.8 / 100
0.10800000000000001

WAT?!

Background: Type Coercion

I’m putting the cart before the horse a bit here, but I’m going to go ahead and spoil bits of the rest of this post by telling you up-front that many of these WATs involve type coercion. JavaScript is a dynamically typed language, but it is also a weakly typed language, meaning that variables may be automatically changed from one type to another in fairly wide contexts. Coercion is distinct from casting, in which a variable is changed to another type, but only if you’re changing the interpretation of its contained data or if the new type is higher up in the same type hierarchy. For example, in Java when you cast a long to an int, you’re asking that the top 32 bits be ignored and the remaining bits be treated as a 32-bit signed integer; when you cast a String to an Object, you’re asking the compiler to treat calls to the reference as if they were calls to an instance of Object.

Type coercion, on the other hand, is happy to turn an int into a String for you. In a language like Java, it happens only in a very few places, and is usually pretty obvious and easy to predict. If you’re used to these rules, though, JavaScript’s coercion can be pretty surprising. Not only will it turn a Numeric type into a String for you, but it will go the other direction too.

And now, without further ado, on to the list of WATs.

WAT Number One: Empty Array Not Equal To Empty Array

[] == [] => false

There’s actually a lot going on here. At first glance, it looks absurd: an empty array does not equal itself?! But that’s not really the question we asked.

So, what question did we ask? Like a Facebook relationship status, it’s complicated. JavaScript’s double-equals equality operator has a fairly extensive set of rules that make it more difficult to use than the double-equals operator in other C-like languages.

What we’re really asking here is, “Is one instance of an empty array equal to another instance of an empty array?” In JavaScript, the answer is no, because when you’re comparing two instances of the same type, double-equals will return true if and only if they are the same instance. We’ve created two instances here, one on the left side of the operator, and one on the right side of the operator. They are not the same instance, so [] == [] evaluates to false.

WAT Number Two: Empty Array Equals Bang Empty Array

[] == ![] => true

OK, now what is this nonsense?

Although I agree it’s pretty confusing, it’s not nonsense if you know the rules and if you apply them in the right order. Here, the bang operator binds more tightly than the double equals, so the first thing we have to do is evaluate ![]. It so happens that in JavaScript, the bang operator coerces whatever it’s applied to into a boolean value. An empty array, in JavaScript, is considered “truthy”, so when it’s coerced into a boolean, it becomes true, not false, and the bang operator converts the true into a false. Now the question looks more like this:

[] == false => true

OK, but that still looks bizarre, right? We just said that an empty array was truthy, not falsy, and true certainly does not equal false!

But, like I said, double-equals is complicated. The rules say that when you’re comparing an Object on the left and a boolean on the right, you convert both sides to numbers (the same as applying the Number() function), and re-run the comparison.

In JavaScript, Number([]) => 0, and Number(false) => 0, so now we have a new question:

0 == 0 => true

That’s more like it. Yes, zero is equal to zero.

WAT Number Three: Empty Array plus Empty Array Yields Empty String

[] + [] => ''

This is actually fairly straight-forward once you understand that the addition operator in JavaScript only operates on two types: Strings and Numbers. We’re asking JavaScript to add two Arrays, which it doesn’t know how to do, so it coerces them both into Strings and then concatenates them together. Calling [].toString() yields an empty string, and concatenating two empty strings together yields an empty string.

WAT Number Four: Empty Array minus Empty Array Yields Zero

[] - [] => 0

This case is very similar to the previous case, except we’re using the minus operator instead of plus. Again, JavaScript doesn’t know how to subtract arrays, and while Strings can be concatenated with the plus operator, Strings don’t understand the minus operator at all, so JavaScript coerces both sides into Numbers for us, instead of Strings. If you call Number([]) in JavaScript, you get 0, so what we’re really saying is:

0 - 0 => 0

That makes a lot more sense.

WAT Number Five: An Array Of Values Is Equal To A Weird Looking String

[ null, undefined, [] ] == ',,' => true

Woah, hey now. What’s this all about? It’s all about coercion and that double-equals operator again.

Here, we are using double-equals to compare an Object and a String. The rules say that when you’re comparing an Object on the left side and a String on the right side, you have to perform some operations to the left hand side to figure out how to do the comparison. The end result of this is that the array on the left is coerced into a String (by calling its toString() function, if it has one). And it just so happens that calling [null, undefined, []].toString() yields the String “,,” /(Why? Because null, undefined, and [] all turn into empty strings, and JavaScript joins those empty strings with commas)/. So we’re really asking if two strings are equal:

',,' == ',,' => true

Yes, they are, so it evaluates to true.

WAT Number Six: Array Plus Object Yields The String “[object Object]”

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

This is still more type coercion. We’ve already discussed how the addition operator only operates on Numbers and Strings, and what’s happening here is JavaScript converting both sides of the operator into Strings. An empty array, as we’ve already seen, gets converted into an empty string. But what about {}?

In JavaScript, {} is (usually!) an Object literal. It’s really saying, “Make an instance of Object for me, please.” So now we’re concatenating an empty string, and the result of coercing an Object instance into a String.

Well, the default toString() for Object returns the string “[object Object]”. The code now becomes:

'' + '[object Object]' => '[object Object]'

WAT Number Seven: Object Plus Array Yields Zero

{} + [] => 0

BUH!

Wait wait wait. We just got finished explaining how JavaScript coerces both sides into a String, right? So shouldn’t this be exactly the same as the previous example?

Well, not quite.

We’ve actually been bitten by a bizarre edge case here. JavaScript has misunderstood our intentions completely. As it scanned the line, it interpreted the initial {} not as an object literal, but rather as an empty code block. Yes, really. In essence, it has just decided to ignore it completely, and treat the line as if it were written this way:

+ [] => 0

That in itself would seem nonsensical, except that JavaScript also allows unary addition operator. Unary addition + x is equivalent to calling Number(x), so what you end up with is an empty Array instance being converted to a number, which yields 0.

WAT Number Eight: Math.min() is greater than Math.max()

Math.min() < Math.max() => false

Before we explore this, let’s look at how these functions are defined, and what they return.

Math.min() and Math.max() are being used deceptively in this WAT. Most languages define global min and max values, and JavaScript does too—but not with Math.min() and Math.max(). Instead, JavaScript gives us Number.MIN_VALUE and Number.MAX_VALUE, and these behave exactly as you would expect:

> Number.MIN_VALUE < Number.MAX_VALUE
true
> Number.MIN_VALUE > Number.MAX_VALUE
false

Math.min() and Math.max(), on the other hand, are functions used to find the min and max values of a sequence of numbers. If you use them correctly, they make sense:

> Math.min(27, 9, 13)
9
> Math.max(27, 9, 13)
27

And if you call them without arguments?

> Math.min()
Infinity
> Math.max()
-Infinity

Why? It’s how they’re implemented. Math.min() initializes its return value to Infinity and then looks for the smallest number in the sequence. There isn’t one, so it returns Infinity. Similarly, Math.max() initializes its return value to -Infinity, and looks for the largest value. There isn’t one, so it returns -Infinity.

The final form of this makes a lot more sense:

Infinity < -Infinity => false

WAT Number Nine: Floating Point Madness

10.8 / 100 => 0.10800000000000001

This is probably the sanest WTF on the page. If you’ve ever worked with floating point numbers in any language, this kind of thing is probably already familiar to you. It’s just a run of the mill floating point accuracy issue.

But 100 is an integer, isn’t it? Well, no. Not in JavaScript. JavaScript numbers come in only one flavor: IEEE-754 floating point. There are no integers in JavaScript. This may in fact be the single most important thing to know about JavaScript, especially if you’re doing any kind of math.

This problem would present itself in C, too. Example:

#include "stdio.h"

int main(void) {
    float f = 10.8f / 100.0f;
    printf("%.16f\r\n", f);
}

If you run this code, what do you get? Not 0.108! On my computer, you get 0.1080000028014183, because of inherent limitations in floating point accuracy.

JavaScript isn’t alone here, it’s just more likely to bite you because you have to use floating point, while you’re free to use other numeric types in other languages.

Conclusion

I’ll be frank: JavaScript is weird, but it’s here to stay, and if you find yourself working in it, it’s good to immerse yourself in the weirdness and try to get a feel for what’s going on under the hood. I hope this post has helped to clear up a handful of the stranger seeming cases, especially if you’re new to it and coming from the background of another language.

Comments