Tom MacWright

tom@macwright.org

JavaScript wats, dissected

JavaScript still gets quite a bit of flak for being weird. I’m going to dig in to some of the most commonly lampooned features and ask whether it really deserves that reputation.

Sorting

> [14, 1, 2, 3].sort()
[ 1, 14, 2, 3 ]

What this really digs at is how does your language treat comparisons between different types? Examples of different approaches:

Python treats all strings as greater-than than numbers:

>>> "1" > 2
True
>>> sorted(["1", 2])
[2, '1']

Ruby doesn’t allow comparisons between strings and integers, so sorting mixed lists throws an error:

irb(main):004:0> "1" > 2
ArgumentError: comparison of String with 2 failed
	from (irb):4:in `>'
	from (irb):4
	from /usr/local/bin/irb:11:in `<main>'
irb(main):005:0> ["1", 2].sort
ArgumentError: comparison of String with 2 failed
	from (irb):5:in `sort'
	from (irb):5
	from /usr/local/bin/irb:11:in `<main>'

JavaScript, instead, chooses to convert numbers to strings when they are compared against strings, and thus we get this odd sorting result.

simple-statistics uses numericSort, which is a simple function using a custom sorter:

function numericSort(x /*: Array<number> */) /*: Array<number> */ {
    return x
        // ensure the array is not changed in-place
        .slice()
        // comparator function that treats input as numeric
        .sort(function(a, b) {
            return a - b;
        });
}

parseInt

parseInt is a method that

Now that we’ve explained the algorithm, check out a simple example:

parseInt('123');
// 123
  1. The value is a string, so it doesn’t need to be converted
  2. The radix is assumed to be the default value, 10
  3. It sees, in sequence, 1, 2, and 3, and constructs the value 123.

A slightly less simple example:

parseInt('100', 2)
  1. The radix is provided as 16, so instead of interpreting this numnber as an arabic numeral, we’re parsing it as hexadecimal
  2. Thus the value is 2^2 = 4

And now a ‘wat’ example:

parseInt(NaN, 32)
// 23895
  1. The value is not a string, so it is converted to a string. NaN.toString() is the string NaN
  2. The radix is given as 32, so in addition to the hexadecimal characters of 0123456789abcdef, we support letters up to v. We can confirm that by parseInt('v', 32) = 31 and parseInt('w', 32) = NaN
  3. The letters N and a are valid in base 32 (JavaScript ignore case in this instance, so n is the same as N), and evaluate to 23, 10, and 23. In base 32, each space counts for 32^n, so that equates to (going right to left because that’s the direction the number grows in
    1. ‘N’ = 23
    2. ‘aN’ = 23 + (10 * 32) = 343
    3. ‘NaN’ = 23 + (10 * 32) + (23 * Math.pow(32, 2)) = 23895

Is this unreasonable? I think the conversion to strings strikes people as odd, and you can certainly create zany examples:

var x = { toString() { return '10' } }
parseInt(x)
// 10

But parsing the string "NaN" with base 32 works the same way in virtually every language.

There’s also the fact that parseInt ignores garbage after the number. This it has in common with C’s strtol method, but strtol solves a much harder problem: it scans strings, extracting numbers out of them, and can tell you where the garbage part of the string starts, and can parse multiple numbers from the same string.

Numbers are weird

This one’s well-worn territory:

> 0.1 + 0.2
0.30000000000000004

Why doesn’t 0.1 + 0.2 equal 0.3? Well, computers usually aren’t doing decimal math. They helpfully read our arabic numbers, and as we’ve seen, lots of other number representations, but the internal representation and the operations on that representation don’t resemble math class.

Floating-point arithmetic is identical between JavaScript and other programming languages: 0.1 + 0.2 will display as 0.30000000000000004 in Python, Ruby, Haskell (ghci). PHP will display the result as 0.3, but that’s because its echo function automatically formats the number for beauty. Testing whether the value equals 0.3 reveals that it doesn’t.

There are a few exceptions, but it’s very unlikely you’re using them. The bc calculator you already have installed if you’re using macOS or Linux is an arbitrary precision calculator that stores numbers as decimal numbers and does arithmetic on them in decimal, so it returns 0.3 for the result of 0.1 + 0.2, and, indeed, that’s the case. The same goes for the dc calculator you also have installed.

JavaScript does deviate from other scripting languages in the fact that it doesn’t have integer numbers. All numbers, regardless of whether they have a fractional part, are represented as floating point, which means that integer accuracy only goes up to 53 bits, not the 64 you can get elsewhere, and there are no safeguards against adding an integer to a float and getting a float out of the equation.


So, some takeaways here:

July 29, 2017 @tmcw

Write JavaScript? I've written many other articles on the topic.