💾 Archived View for yujiri.xyz › software › javascript.gmi captured on 2024-02-05 at 09:43:06. Gemini links have been rewritten to link to archived content
⬅️ Previous capture (2023-09-08)
-=-=-=-=-=-=-
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 am a fierce opponent of dynamic typing, and have written about benefits of static typing besides the obvious. Javascript defenders will likely point to the existence of TypeScript, but a bolted-on type system will never be as good as a built-in one (argued in the linked article):
Subtle benefits of static typing
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, indexing an array out of bounds or accessing a nonexistent property of an object gives you `undefined`. Good luck debugging that.
And compounding the confusion: `undefined` *is* a valid value in general!
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
Pretty much the only type errors are to index null or undefined or call something that isn't a function.
Let's have fun with some more things that should be errors, but aren't:
// 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. WHAT THE FUCK {} + []; // 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.
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.
Array.splice is a bad 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 (Python vs Javascript):
arr[-5]; arr[arr.length - 5]; // And imagine if arr was longer arr[1:3]; arr.slice(1, 3);
`==` 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, at least.
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 Javascript leads the others in the functional programming department with 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 *1* 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.