The category discussions about weird JavaScript things is a large and amusing one, probably best exemplified by Gary Bernhardt’s Wat lightning talk.
I’d like to focus on two I haven’t seen discussed as much (though I may
simply have overlooked the canonical article). First, const
doesn’t work
the way it ought to, and second, indexOf
’s magic number thing is godawful.
const
doesn’t solve the problem it ought to solve.
Consider this situation:
let myArray = [3, null, 5, 7, null, 10];
printArrayWithNoNulls(myArray);
// => null
// => null
// => null
// => null
// => null
// => null
console.log(myArray);
// => [ null, null, null, null, null, null ]
Wait, what happened!? Well, it turns out the method has a bug:
const printArrayWithNoNulls = (arr) => {
for(let i = 0; i < arr.length; ++i) {
if (arr[i] = null) {
continue;
}
console.log(arr[i]);
}
}
Do you see it? We used =
instead of ==
or ===
. Not only does the method
not work, we’ve also lost our data.
In JavaScript, arrays are pass-by-reference, with the pointer allowing mutation. Let’s redefine our method so it requires a const reference:
const printArrayWithNoNulls = (const *int arr) => { //...
Haha, just kidding. There’s no such thing. Okay, but here’s the real problem: we should change our first line of code:
const myBulletproofArray = [3, null, 5, 7, null, 10];
Let’s do it again. Now at least we’ll have a runtime error showing that we’re
trying to mutate our const
value:
printArrayWithNoNulls(myBulletproofArray);
// => same output, no runtime error. Uh oh...
console.log(myBulletproofArray);
// => [ null, null, null, null, null, null ]
So, uhh, const
, not helping very much, are you?
Okay, I’m going to make a protective copy, then go back to that if things go south.
const myArray = [3, null, 5, 7, null, 10];
const backupArray = [...myArray];
printArrayWithNoNulls(myArray);
if (myArray !== backupArray) {
const myArray = backupArray;
// => Thrown:
// => TypeError: Assignment to constant variable.
// Same eror without `const`
}
console.log(myArray);
Jeez JavaScript, now you pipe up?
So that’s my problem with const
: I want it to be usable as a tool to
protect against the (mandatory) pass-by-reference semantics for non-scalar
values in JavaScript. The dangers in sharing a pointer to a mutable value are
well known: race conditions and inadvertent mutation. In Rust (and C++,
etc.), you must opt in to both pass-by-reference and mutability. In Rust you
have to mark both things in the function definition: the parameter is mutable
and pass-by-reference. You must also mark the variable to be passed to the
function as mutable when you declare it, and also mark that the argument
is mutable and pass-by-reference at the call site:
// `&` means pass-by-reference, `mut` means mutable
fn print_array(arr: &mut Vec<i32>) {
for val in arr {
println!("{}", val);
}
}
fn main() {
// `mut` means mutable
let mut my_array = vec![1, 2, 3];
// `&` means pass-by-reference, `mut` means mutable
print_array(&mut my_array);
}
Indeed, const
in JavaScript affects what may be the least important
aspect of mutability: it simply prevents you from rebinding a different value
to the same variable name. And indeed, in Rust, as long as you use let
again to show that you’re declaring a new variable (albeit one with the same
name), you’re fine:
let x = 7;
let x = 10; // No problem
let y = 15;
y = 20; // error: cannot assign twice to immutable variable `y`
let mut z = 20;
z = 30; // No problem
indexOf
’s magic number behavior is really, really atrocious.
Consider this:
const myArray = ["foo", "bar", "baz"];
const containsValue = (arr, value) => !!arr.indexOf(value);
containsValue(myArray, "bar"); // => true
containsValue(myArray, "baz"); // => true
Excellent, ship it! Well, let’s try to see if we get any false positives:
containsValue(myArray, "quux"); // => true
Oh, shoot, looks like our code always returns true
.
But wait, we overlooked one test case:
containsValue(myArray, "foo"); // => false
We have written a function that returns true
in all cases except when we
look for the first element of the array.
Why? It has to do with indexOf
returning a number—the index—not a boolean.
What happens if a value is missing from the array? Given the foreseeable use
cases, together with the fact that all JavaScript values can be implicitly
coerced to booleans, surely care has been taken to ensure that the value
returned is falsey. So does indexOf
return null
to indicate that there is
no index that has our number? Or does it return NaN
to signify that there
is no number that can index us to where we want to go? Or false
itself, to
signify that it is false
that the array has the value? All of those would
coerce to false.
No, none of those. It’s a number.
Okay, well I’ve got it then: of the 18,437,736,874,454,810,623 distinct
numbers that can be expressed in JavaScript,1 precisely one is
falsey: 0
. That has to be it, right?
Nope, can’t be: the item could be at the zeroth position of the array. It’s
starting to sound like this shouldn’t be a number. What number is it,
though? The answer is -1
. As in, a truthy value. As in, an essentially
random magic number. As in, a number whose only benefit is that it is not a
valid index into an array. But of course that is also the case for all
negative numbers, all floating-point numbers, and for null
, undefined
,
NaN
, and false
.
Now that magic constant, -1
, has to leak into our code, which we
otherwise endeavor to keep free of magic constants. And if we assume that
since number coerce to booleans, we can rely on getting the right behavior
for the value not found case, we’re going to introduce a bug. Grotesque.
Jeffrey Sax, Answer, How many distinct values can be stored in floating-point formats?, Stack Overflow, https://stackoverflow.com/a/7744178/3396324 (Oct. 12, 2011). ↩︎