Forbidden JavaScript

You know, I actually like JavaScript.

The language gets a bad rap, and honestly for good reason. But from ECMAScript 2015 onward, it’s received fantastic additions that have made it my personal favorite scripting language. It’s just that those additions are built upon a foundation of questionable decisions that have been baked into the language and can’t be reversed for backwards compatibility (typeof null, anyone?). But I suppose that’s what happens when you’ve only got ten days to make a programming language.

This article is a collection of those unfortunate choices. The obscure corners of the language better left untouched. The features so great, they had be disabled in strict mode.

I’m going to be dunking on JavaScript in this article. But in fairness, there will be no jabs at JavaScript’s… special type system here. Because frankly, it’s already been done to death.

Caller and Callee

All Functions have a property named caller which gives you the function that’s currently calling that function.

function f() {
	console.log(f.caller); // [Function: g]
	console.log(g.caller); // [Function: h]
	console.log(h.caller); // null
}

function g() {
	f();
}

function h() {
	g();
}

h();

Similarly, arguments.callee will tell you what function is currently being executed. This isn’t very useful since you already have that information, but according to this StackOverflow answer, in earlier versions of JavaScript it was situationally necessary for implementing recursion.

Aside from preventing certain kinds of optimizations, letting functions know where they’re being called from is just asking for people to write downright diabolical code. I suppose if you really wanted to make the argument for caller/callee, they could potentially be used for debugging, but— don’t. console.trace() exists. Use it.

The with Statement

The with statement brings all of an object’s properties into the current scope. In a sense, it makes the specified object act like the global object temporarily.

function areaOfCircle(r) {
	with (Math) {
		return PI * pow(r, 2);
	}
}

The with statement has the advantage of slightly reducing the amount of keystrokes you need to perform in some situations. The disadvantages?

For starters, it makes code harder to read because you can no longer tell where identifiers are being defined. Looking at the above example, you can’t tell by just looking at the code if PI is a part of Math or defined in an outer scope. The same ambiguity also limits the optimizations JavaScript engines can make.

It also creates potential future compatibility problems. Looking at the example above again, if Math added a new property called r, our code would break because it would shadow the argument we were trying to access. This concern led to the addition of @@unscopables, a hack that exists only to defend against use of with.

Globals for HTML Elements

Here’s a common pattern in web apps: Give an element on the page a unique identifier with the id attribute, then use Document.getElementById() to get a reference to it in the DOM.

<div id="my_div">Hello world</div>

<script>
	const myDiv = document.getElementById('my_div');
	console.log(myDiv); // <div id="my_div">…</div>
</script>

But what if we didn’t need getElementById()? What if we actually had a reference all along?

<div id="my_div">Hello world</div>

<script>
	console.log(my_div); // <div id="my_div">…</div>
</script>

Yep, using the id attribute on any element makes a global with the same name. This also works for the name attribute on certain elements.

Don’t believe me? Here’s the spec:

The Window object supports named properties. The supported property names of a Window object window at any moment consist of the following, in tree order according to the element that contributed them, ignoring later duplicates:

  • window's document-tree child navigable target name property set;
  • the value of the name content attribute for all embed, form, img, and object elements that have a non-empty name content attribute and are in a document tree with window's associated Document as their root; and
  • the value of the id content attribute for all HTML elements that have a non-empty id content attribute and are in a document tree with window's associated Document as their root.

Labeled Statements

The goto was once a standard part of the programmer’s control flow toolkit, but after decades of criticism and debate, including Dijkstra’s infamous Goto Statement Considered Harmful, it’s now a rarity. JavaScript isn’t cursed enough to have proper gotos, but it does have a close cousin.

You can give any statement a name, making it a labeled statement. Their only use is with the break and continue statements, which accept an optional label name.

outerBlock: {
	innerBlock: {
		break outerBlock;
		console.log('1'); // skipped
	}
	console.log('2'); // also skipped
}
console.log('3');

The implementation is rather constrained when compared with the chaotic energy of a proper goto, since you can only jump to labeled statements that you’re currently nested within. But I’d go as far as to say that, in contrast with the rest of this article, this is actually a pretty useful feature, particularly for breaking out from within nested loops. Yet I don’t think I’ve ever seen this used in the wild, or discussed as more than an obscure curiosity.

But the labeled statement has recently seen a second life. The developers of Svelte saw this obscure piece of syntax laying around and— yoink. Since you can label any statement, they decided to add $: as special bit of syntax in their compiler, and now any statement labeled with $ becomes “reactive”.

// Non-reactive assignment (LAME)
let y = x + 1;

// Cool reactive assignment
$: z = x + 1;

HTML Comments

In addition to line comments (//) and block comments (/* */), JavaScript also supports HTML-style comments (<!-- -->). This works in <script> tags as well as in .js files (so long as they’re not loaded as a module).

<!-- Print a message to the console -->
console.log('Hello world');

They may look like HTML comments, but they don’t work like them. While comments in HTML are functionally equivalent to JS’s block comments, they actually act as line comments in JavaScript, and you’re under no obligation to match the opening and closing comments.

Why does this exist? Well, it goes all the way back to the beginning of JavaScript. The concern was that if you loaded a page with <script>s in a browser without JavaScript support, the script’s source code would be displayed on the page since the browser didn’t know not to render it. So the idea was that to avoid this, you could wrap your scripts in an HTML comment. To a JavaScript-supporting browser, the comment would be quietly ignored and the script executed; to browsers without JS, any text in the <script> tags would be commented out and not appear on the page. Decades later this concern doesn’t exist, but for backwards compatibility it’s stuck.

<script>
	<!--
	// JavaScript here
	-->
</script>

But my absolute favorite part is just how much effort goes into supporting this obscure, obsolete feature.

Within the context of JavaScript, the syntax of an HTML comment becomes ambiguous. You might have noticed that <!-- is the same as < ! -- (less than, logical not, prefix decrement) except with the whitespace stripped out (watch out for that one, minifiers!). And in a module file, <!-- is indeed treated as those three separate operators.

You also have to deal with the similarity to <!- (less than, logical not, unary negation). Disambiguating requires the lexer to consider four characters at once, the most required in the language. V8’s lexer has a special case coded in just for dealing with HTML comments.

The scanner chooses a specific scanner method or token based on a maximum lookahead of 4 characters, the longest ambiguous sequence of characters in JavaScript.

Blazingly fast parsing, part 1: optimizing the scanner

All this to support backwards compatibility of a feature that was added for backwards compatibility with browsers from thirty years ago.

JavaScript.