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 Function
s 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.
with
Statement
The 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 aWindow
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 allembed
,form
,img
, andobject
elements that have a non-emptyname
content attribute and are in a document tree with window's associatedDocument
as their root; and- the value of the
id
content attribute for all HTML elements that have a non-emptyid
content attribute and are in a document tree with window's associatedDocument
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 goto
s, 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.
All this to support backwards compatibility of a feature that was added for backwards compatibility with browsers from thirty years ago.
JavaScript.