WTF.js - javascript corner cases and pitfalls
It is an understatement to say that JavaScript is a quirky programming language. Even seasoned developers now and then trip over some obscure corner cases, and end up wondering why the language was designed that way. I have collected a few of those pitfalls for you, in the hope that you will be less surprised next time you stumble upon them.
This post is a write-up of a tech talk I gave at SBB. The slides can be found here.
Question 1: n/a
What is the result of the following expression?
"N" + "a" + +"a" + "a"
“NaNaNa”
- A
SyntaxError
370
Naaa
Answer
1. “NaNaNa”
Here's the reason why:
- The expression is evaluated as ”N” + “a” + (+”a”) + “a”
- The unary + operator converts (+”a”) to NaN
- The addition operator (+) converts NaN to “NaN”
The resulting string is therefore `“NaNaNa”`
Question 2: To the infinity and beyond
What is the result of the following expression?
parseInt("Infinity", 19)
18
Infinity
NaN
undefined
Answer
1. 18
Here's an excerpt of the corresponding section in the JavaScript reference:
18.2.5 parseInt ( string, radix )
The parseInt function produces an integer value dictated by interpretation of the contents of the string argument according to the specified radix. Leading white space in string is ignored. If radix is undefined or 0, it is assumed to be 10 except when the number begins with the code unit pairs 0x or 0X, in which case a radix of 16 is assumed. If radix is 16, the number may also optionally begin with the code unit pairs 0x or 0X.
[…]
NOTE
parseInt may interpret only a leading portion of string as an integer value; it ignores any code units that cannot be interpreted as part of the notation of an integer, and no indication is given that any such code units were ignored.
Always read the fine print!
Question 3: The trinity operator
What is the result of the following expression?
[] == "0"
NaN
false
true
undefined
Answer
2. false
Seriously? Well, there are other confusing relationships out there:
All joking aside, here's the formal definition of the `==` operator:
7.2.14 Abstract Equality Comparison
The comparison x == y, where x and y are values, produces true or false. Such a comparison is performed as follows:
- If Type(x) is the same as Type(y), then return the result of performing Strict Equality Comparison x === y.
- If x is null and y is undefined, return true.
- If x is undefined and y is null, return true.
- If Type(x) is Number and Type(y) is String, return the result of the comparison x == ! ToNumber(y).
- If Type(x) is String and Type(y) is Number, return the result of the comparison ! ToNumber(x) == y.
- If Type(x) is Boolean, return the result of the comparison ! ToNumber(x) == y.
- If Type(y) is Boolean, return the result of the comparison x == ! ToNumber(y).
- If Type(x) is either String, Number, or Symbol and Type(y) is Object, return the result of the comparison x == ToPrimitive(y).
- If Type(x) is Object and Type(y) is either String, Number, or Symbol, return the result of the comparison ToPrimitive(x) == y.
- [otherwise] Return false.
In summary, don’t use the == operator!
Question 4: Revolution 9
What is the result of the following expression?
9999999999999999 + 1.1
10000000000000000
10000000000000000.1
10000000000000001
10000000000000002
Answer
4. 10000000000000002
Unlike Java, JavaScript only has one numerical data type, namely IEEE 754 double precision floating point numbers. These numbers have an increasing loss of precision, up to the point that gaps start to occur between integers (this happens from 2^53 onwards) An image is worth a thousand words:
Therefore we may use integers only between Number.MIN_SAFE_INTEGER and Number.MAX_SAFE_INTEGER
Question 5: sort me if you can
What is the result of the following expression?
[10, 3, 2, 1].sort()
[]
[10, 3, 2, 1]
[1, 10, 2, 3]
[1, 2, 3, 10]
Answer
3. [1, 10, 2, 3]
Again, here's the corresponding section in the JavaScript language reference:
Array.prototype.sort()
The sort() method sorts the elements of an array in place and returns the sorted array. The default sort order is ascending, built upon converting the elements into strings, then comparing their sequences of UTF-16 code units values.
The time and space complexity of the sort cannot be guaranteed as it depends on the implementation.
Question 6: lisp
What is the result of the following expression?
(![] + [])[+[]] +
(![] + [])[+!+[]] +
([![]] + [][[]])[+!+[] + [+[]]] +
(![] + [])[!+[] + !+[]];
"fail"
"true"
"lisp"
"null"
Answer
1. "fail"
![] evaluates to false, and adding [] to it turns it into the string "false". On the other hand, +[] evaluates to 0. The first line therefore results in the string f.
!+[] evaluates to true, and +!+[] to 1. The second line therefore results in the character at offset 1 in "false".
[![]] evaluates to [false], and [][[]] to undefined. When they are concatenated together, the result is "falseundefined". The second half of the expression consists of +!+[] that evaluates to 1, and [+[]] that evaluates to 0. Concatenated together, the result is "10". The third line therefore evaluates to "i".
Similarly, the fourth line evaluates to the character at offset 2 in "false", i.e. 1.
Question 7: constructor3
What is the result of the second statement?
const c = "constructor";
c[c][c]('return "wat?"')();
'return "wat?“’
undefined
“constructor”
“wat?”
Answer
4. “wat?”
In order to understand why that is the case, let's evaluate the expression incrementally: c obviously evaluates to "constructor", and c[c] to the String() constructor (a function that takes anything as input and creates a string out of it). Because of that, c[c][c] returns the Function() constructor, that takes a string (i.e. the function body) as input and returns a Function object. c[c][c]("return 'wat?'") then obviously evaluates to "wat?".
Question 8: timeouts
In which order will the alerts be displayed?
setTimeout(() => alert("1"));
Promise.resolve().then(() => alert("2"));
alert("3");
- The order is undefined
- 3 - 2 - 1
- 1 - 2 - 3
- 3 - 1 - 2
Answer
2. 3 - 2 - 1
A common misconception is that all asynchronous tasks get enqueued in a single queue in JavaScript. That's not the case however: the event loop differentiates between micro and macro tasks, that have an own queue each. The event loop processes all microtasks first, before picking up the next macrotask (cf. picture below). Since promises are enqueued on the microtask queue, and the setTimeout callback in the macrotask queue, the order is 3 - 2 - 1.
Question 9: Proteus
What will be printed in the console?
var proteus = "lion";
var show = function() {
console.log(this.proteus);
};
var o = {
proteus: "pig",
show: show
};
o.show();
show();
function F() {
this.proteus = "snake";
this.show = show
}
new F().show();
var f = new F().show;
f();
- lion - lion - lion - lion
- pig - lion - snake - lion
- pig - undefined - snake - snake
- pig - lion - snake - snake
Answer
2. pig - lion - snake - lion
In o.show(), this refers to o, therefore pig is displayed first. In show() however, this refers to window and lion is displayed next. In new F().show(), this refers to the new instance of object F and snake gets printed out to the console. Lastly, in f() this refers to window, which means that lion will be printed out again.
Conclusion
In Brendan Eich’s defense, Netscape supposedly gave him only ten days to create the first JavaScript version. Imperfections were unavoidable, and since the language became incredibly popular overnight, they were almost impossible to remove later on. Despite all of its shortcomings, JavaScript still remains immensely popular 25 years later.
Did trip over an obscure corner case too? Then let me know about it on Twitter!
Happy hacking :)