💾 Archived View for yujiri.xyz › software › javascript.gmi captured on 2022-07-16 at 14:28:03. Gemini links have been rewritten to link to archived content
⬅️ Previous capture (2022-06-03)
-=-=-=-=-=-=-
I find it bizarre that people choose to use Javascript when they have other options. I will attempt a comprehensive, neutral analysis of it's quality as a language as I do for other languages.
Best comparison: Python review
I've written at length about why I oppose dynamic typing, and workarounds like TypeScript can at best mitigate the harm.
But Javascript is even worse here than other dynamic languages. In others, like Python, most things that you'd want to be compile-time errors are still run-time errors. But in Javascript they're often silent failures. For example, accessing a nonexistent slot of an array or object gives you `undefined`. Good luck debugging that.
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.length; // 1
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.
You also don't get an error when passing too many arguments to a function.
function f(param) { console.log(param) }; f(1, 2, 3); // Just prints 1
Even indexing a non-array and non-object isn't an error:
5[5]; // undefined
The only thing that *is* an error to index is null or undefined.
Javascript arrays aren't really arrays, but objects. I don't just say this because `typeof [] === 'object'`; there are a lot of harmful ways in which the language doesn't seem to think of them as an actual sequence type. 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
See what I mean? It's like you're just assigning keys in an object, and array indices don't have any special meaning (though they do print sensibly).
And those empty items *aren't the same as undefined* (if they were, that would imply a deeper difference between arrays and objects than Javascript seems to want to admit). Or they are, but they're not. Check this out:
emptyArr = []; arrEmpty = [,,,]; arrUndefined = [undefined, undefined, undefined]; console.log(emptyArr[0], arrEmpty[0], arrUndefined[0]); // undefined undefined undefined console.log(emptyArr.length, arrEmpty.length, arrUndefined.length); // 0 3 3 emptyArr.map(i => console.log('found item:', i)); /// prints nothing arrEmpty.map(i => console.log('found item:', i)); /// prints nothing arrUndefined.map(i => console.log('found item:', i)); /* prints: found item: undefined found item: undefined found item: 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. Pretty bad newb trap in my opinion.
Array.splice is a poor API; it is both for deleting and for inserting elements. There should be separate methods for each.
Because of the way Javascript treats arrays as objects, it supports neither negative indices nor slicing. Just compare the readability difference:
arr[-5]; arr[arr.length - 5]; // And imagine if arr was longer arr[1:3]; arr.slice(1, 3);
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'; // 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 /. ?!?!?!
Most of these are unintuitive footguns if not complete nonsense. An operation that doesn't involve numbers should never come out as `NaN`; that's not what `NaN` means.
In general, things that are almost certainly mistakes should raise exceptions, not silently return a nonsensical value.
`==` on objects (including arrays) compares for identity, not equality. If you want to test whether two objects are equal, you have to iterate over their keys.
In a language that has `==` and `===`, you would think `==` would compare by value for objects, and `===` would compare identity. But no, in the one case where having two equality operators might actually make sense, they do the same thing.
x = 5; y = new Number(5); x == y; // true x === y; // false typeof x; 'number' typeof y; 'object'
There's literally no point to the existence of these; it's just a newb trap that exists because of how constructors work in Javascript.
And this one is even funnier:
val = new Boolean(false); !!val; // true
Because objects are always true.
Javascipt uses exceptions like other dynamic languages, but it's lacking over Python and Ruby in that it doesn't support catching only specific types of exceptions. `catch` always catches everything and you have to manually check and reraise if you only wanted to catch some kinds. And like the others, it catches name errors. Ugh.
Why do all the dynamic languages catch name errors by default?
It does give good stack traces, and has the finally statement.
The `var` keyword is an unfortunate historical wart. `let` and `const` are the correct ways to declare variables in modern Javascript; `var` is function-scoped instead of block-scoped, and has the bizarre behavior of applying to references *before* itself: if you have naked `foo = 5` and then later in the function `var foo;` there is no error and the value of `foo` will be `5` right after `var foo;`.
Most dynamic languages have `map`, `filter`, `reduce`, and lambdas, but I think Javascript leads the others in the functional programming department with arrow functions:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions
They are more concise than the `function` keyword, and the syntax is intuitive too; it *looks* like you're taking the parameter list and doing something with it. Python has lambdas and in-function `def`, but lambdas are limited to just a `return` statement and `def` doesn't handle scoping the same way arrow functions do.
The NPM ecosystem is plagued worse than any other package ecosystem with overdependency. You can barely install anything without populating your `node_modules` with at least a hundred directories. This means it's impossible to audit your dependencies in any significant Javascript project.
Dependencies and maintainers by Drew DeVault
A lot of the dependencies are nonsense packages, too, which provide a single function of often just *one* line.
This article is a good read on the situation
Ecosystems of other languages don't have this problem. Even Django, the giant all-the-features Python web framework, has only *3* dependencies last I checked, including indirect.