I know Javascript is a language a lot of people already agree is terrible, but now that I've got a substantial amount of experience with it myself, I think it'll be fun to write a language opinion article where I get to be more ranty. Of course, in keeping with my tradition, I'll have to open with the good things first. How Javascript And I Met isn't particularly interesting (I just chose to learn it because of its unique ability to run in a browser and because it was a bad thing not to have on my resume as a programmer), so I'll skip that.

Also, I'll stick to talking about plain Javascript here, not the newfangled Web Components stuff. I'll write what I think of those later. I don't think I'm experienced with them enough to judge them yet.

Interactive prompt

Well, at least you've got the interactive prompt. Any browser's devtools allow running JS interactively, and the Node.js command-line tool also has an interactive interface. This is really nice, I'll admit.

The type correction features most browsers' devtools I've seen foist on you are disgusting. When I'm typing a name it always opens a suggestion box that covers the rest of the command history if there's any name defined that starts with what I have, and it fucking rebinds the enter key to "accept suggestion", so I get hoodwinked by that on a regular basis.

But the Javascript command-line experience has some compensatory advantages. Due to the nature of browser devtools, it's an out-of-the-box feature to be able to use it interactively while the page is running. That's pretty damn useful.

I actually love how it handles objects.

Javascript objects are dictionaries. They're just mappings of string keys to values.

I sincerely think that if you don't have any type safety whatsoever in your language and objects are going to be totally mutable, you should not distinguish between objects and dicts, because that loses all the meaningful differences.

No compile-time sanity checking

A repeated point from my opinion on Python, the flipside of being interactive. You'll never know your JS is even valid until you run it and test every code path.

"Just use TypeScript! Problem solved!"

No, problem not solved. Using TypeScript adds a requirement for me to compile my JS before I run it. I'm not saying TypeScript isn't good or helpful (I haven't used it), but it's not built-in, it's a separate tool you have to learn, and even then it introduces a large drawback of its own.

Trying to access a nonexistent slot of an array or an object silently returns undefined

But you can define one of the values to be undefined and it's now in there!

arr = [undefined];
arr[0]; // undefined
arr[1]; // undefined
arr == []; // false

By the way, even a function parameter just gets undefined if it's not passed. All arguments are optional; you can't define a function that requires you to pass it a parameter. Let that sink in for a minute.

Arrays aren't really arrays

This took me a while to understand, but arrays are really just objects with numbered fields. (No wonder you can't add arrays in the way you'd expect...) This has many bad corellaries. One is that you can assign past the end of an array and you just get "empty items" inbetween;

arr = [];
arr[5] = 'x';
arr; // [<5 empty items>, 'x' ]
arr.length; // 6
delete(arr[5]);
arr; // [ <6 empty items> ]
arr.length; // 6

And note that those empty items aren't the same as undefined. Or they are, but they're not. Check this out:

emptyArr = [];
arrEmpty = [,,,];
arrUndefined = [undefined, undefined, undefined, undefined];
emptyArr == arrEmpty; // false
arrEmpty == arrUndefined; // false
arrUndefined == emptyArr; // false
console.log(emptyArr[0], arrEmpty[0], arrUndefined[0]); // undefined undefined undefined

It's like the holy trinity of undefined!

This is because arrays have a length attribute that stores the number of elements they supposedly have. So when you assign to an index, it changes the length, and then when you look at the array all the slots inbetween that don't exist as keys in the array are presented as these "empty items". delete is meant for removing a key from an object, so when used on an array, it only deletes the key and doesn't collapse the others or modify the length attribute, so it just leaves an empty slot behind.

Arrays and objects represent fundamentally different ideas. I know dynamic typing obscures this, but arrays exist to represent a set of values of the same type. That's what the idea of iterating is based on. You would never iterate on the fields of an object in an object-oriented language. Iterating implies doing the same thing to each element, which necessitates the elements having some common type or interface. If the values are meant for iterating, they're in an array; if they're not in an array, it's because they don't represent a set of comparable things, but different attributes of one thing.

In Javascript, dicts are objects, which means that for an iterable set of values with duplicate prevention and no ordering, you have to use an object. But that doesn't change the truth that arrays are not objects semantically.

Thinking about them as such has led to the aforementioned tragedies, and also the actual ways to work with arrays are counterintuitive. The .push method is how you're supposed to add stuff to the end, and .concat is for adding arrays. The main way to delete from an array is .splice, but there are a lot of others depending on the specifics. This article goes through a bunch of them.

For some reason, .splice is also how you insert elements. The one method is basically a swiss army knife instead of using different functions to accomplish different tasks.

This implicit type conversion is outrageous

A lot of people who rant about Javascript mention this. Let me just jump into the examples:

// Strings and numbers
'q' - 'q'; // NaN
5 + '5'; // '55'
'5' * '2'; // 10
'5' - '2'; // 3
// Arrays
1 + [1]; // '11'
1 + [1, 2]; // '11,2'
1 - [1]; // 0
1 - [1, 2]; // NaN
[] + []; // ''
[] - []; // 0
[1, 2] - [3, 4]; // NaN
// Objects
{} + 0; // 0
{} + ''; // 0
{} - 0; // -0. No, I am not kidding. -0 can be assigned to a variable and it stays that way. On the bright side, it seems to be exactly the same as 0 for every purpose I can find.
{} + []; // 0
[] + {}; // '[object Object]'
{} - []; // -0
[] - {}; // NaN
{} + {}; // NaN
{} - {}; // NaN
{} / []; // SyntaxError: Invalid regular expression: missing /. ?!?!?!

I can see arguing for some implicit type conversion. I hate in Go when you can't even compare int to int32 without an explicit conversion for example. But this? Not only it is through the roof, it's wildly inconsistent and unintuitable.

Iterating is a mess

Javascript has three different for loop constructions: the C-style for (let i = 0; i < items.length; i++) {; then for (let i in items) {, and for (let i of items) {. What are the differences? Can we maybe use these two latter constructions to elide the antiquated C bullshit?

Well, no. for..in is for iterating on the keys of an object... but objects in Javascript have string keys. And do you know what that means happens when you try to use this on an Array?

nums = [5, 16, -3];
for (let i in nums) {
    console.log(i + 1);
}
/* Prints
01
11
21
*/

Because arrays are technically objects and so their keys as given by for..in are of course the string indices. This works for some use cases, but if you try to add to the index counter, it'll break your code in bizarre ways.

for..of, on the other hand, only gives you the values. Not the keys. And of course there's no easy way to get the key from the value; there's nothing equivalent to Python's enumerate, as far as I know. So, we still need to use antiquated C bullshit to iterate in Javascript.

Variable declarations are a mess

Assigning to an undefined variable in Javascript by default creates a global variable, if you don't use 'use strict'; at the top of the file. Besides this unfortunate fact, there are three different keywords for declaring variables that all have subtle differences:

What an elegant and straightforward system!

Boilerplate syntax - semicolons, parentheses around conditions

I talked about this a lot in my review of Python, but Javascript does worse than just having these boilerplate syntax features. Semicolons will usually be automatically inserted by the interpreter, so often you don't need them, but if you lean on that fact, sometimes semicolons will be inserted incorrectly and break your code in bizarre ways. And you can't even say "Just don't lean on the feature", because the nature of the feature precludes that. Everyone forget semicolons sometimes in semicolon languages.

Object constructors for primitive types

x = 5;
y = new Number(5);
x == y; // true
x === y; // false
typeof x; 'number'
typeof y; 'object'

Just why? What is the difference between these two things? I'm sure Number provides some methods or something that primitive numbers don't, but why should it if these two things represent the exact same idea? Why shouldn't whatever works on Number just work on numbers?

No error when passing too many arguments to a function

Ugh... for real?

function f(param) { console.log(param) };
f(1, 2, 3); // Just prints 1

Distinction between null and undefined is confusing and unnecessary

There are two primitive values that represent the lack of a value. null and undefined are semantically different; for function parameters, passing undefined causes the parameter to get its default value. Passing null causes it to get null. Now that's nice and intuitive.

Arrays don't support negative indices

Negative array indices are a super useful feature in other languages for making code more concise and more readable. Javascript, perhaps owing to the way it treats arrays as objects, doesn't support them. Just compare the readability difference:

arr[-5];
arr[arr.length - 5];

Slicing arrays is ugly

Javascript also doesn't support the usual syntax for slicing arrays. Instead you use .slice:

arr = ['a', 'b', 'c', 'd'];
arr.slice(1, 3); // Returns ['b', 'c']

It's not horrible, but it's a good deal more verbose than most other languages.

No passing parameters by name

Function parameters in Javascript can't be passed by name, they have to be passed by position. For a long time I actually thought you could do this because it was working for me, but then I got bitten by the inability later and found out it was working in the first place because I was actually still passing them in the right order; the parameter assignments I was doing were actually assigning to global variables. The second place I tried to use it it was in a file with 'use strict'; enabled so I found out.

It is of course possible to have your function take an object for its parameters, but that's an inadequate replacement because it means the function has to be specifically made to be used this way, so at best that works on functions you write yourself and that you refactor with a more convoluted syntax to get this functionality and also lose the ability to pass them by position while you're at it.

Single versus double quotes - meaningless decision

Another point from my review of Python. Having to constantly make this decision distracts me while I'm coding and breeds inconsistent style.



Comments

There's no authentication or anything. If you want to authenticate sign your comment with a PGP key or something :) Markdown formatting is supported.