Spooky tales to scare your JavaScript developers
Welcome, brave souls, to a realm where the ordinary meets the supernatural, where lines of code transform into incantations. Today, I extend an invitation to journey with me into the depths of arcane knowledge, armed with a dusty tome retrieved from the restricted section of the Miskatonic University. This hallowed grimoire is none other than the enigmatic Necronomicon ECMAScript 2023 specification.
As you gather around, let the flickering candlelight cast eerie shadows upon the walls, for what we are about to explore is not just a collection of spooky tales—it's a journey into the spectral corners of JavaScript language. I want you to know with certainty, dear readers, that as with the best ghost stories these tales are all 100% true. So, steel yourselves, for the phantoms of JavaScript await their awakening, and our expedition into the unknown commences now.
The Cursed Legacy of undefined
Our first tale today is an oldie but a goodie. Let us turn to section 12.7.2 Keywords and Reserved Words.
Here we find a list of all the reserved words in JavaScript.
Syntax
ReservedWord :: one of
await break case catch class const continue debugger default delete do else enum export extends
false finally for function if import in instanceof new null return super switch this throw
true try typeof var void while with yield
The scary part about this list isn't what is there but what is missing. Keen readers may notice that null
is a reserved word but it's diabolical double undefined
is missing from this list. This is because undefined
is implemented as a variable. If we turn to section 19.1.4 we can see the undefined
is defined as a property of the global object that is neither writable nor configurable.
But it wasn't always that way. Some of the most ancient among you may remember these chaotic days before ECMAScript 5 (2009). Until that version of the language undefined
was still a writeable object on the global scope. So the variable undefined
could in fact be redefined.
Not the concept of undefined
, mind you. Just its variable binding in most JavaScript scopes. This led to many defensive coding techniques, such as libraries creating their own undefined
variables or comparing values against the void 0
statement which always returns undefined
. This has become much less of an issue recently. Every modern JavaScript runtime now follows a version of the ECMAScript spec which prevents users from casually re-writing the undefined
variable.
undefined = 'Boooooooooooo!';
console.log({ undefined }); // { undefined: undefined }
However, if on a dark moonlit night you find yourself wanting to give your colleagues a fright, you can still invoke a bit of the chaotic times of the past by wrapping your code in a function with a parameter binding named undefined
.
(function (undefined) {
undefined = 'Boooooooooooo!';
console.log({ undefined }); // { undefined: "Boooooooooooo!" }
})();
Just be careful how you use this power...
The primitive monster
Our next tale is another classic. First we need to check out the dog eared pages of section 7.1.1.1. Here lies the abstract incantation for the ToPrimitive
spell that is used to convert objects into primitives such as strings or numbers.
We learn from these pages that one can define a method named with the built in symbol Symbol.toPrimitive
on an object that will be used by the JavaScript runtime to convert the object to a primitive value.
This spell says JavaScript will attempt to call the Symbol.toPrimitive
method on an object if it thinks the object should be converted to a number or string. Conversion happens in a number of places but the most common is when comparing a value with ==
or !=
. It can also happen when using the value as a key to another object otherObject[myValue]
.
One thing keen readers might notice is the JavaScript doesn't require us to always return the same value. We can use this unfortunate knowledge to create a most sinister object. An object that can be used in illogical statements such as (a == 1 && a == 2)
by introducing some mutation into our Symbol.toPrimitive
method.
let evil = {
index: 0,
[Symbol.toPrimitive](hint) {
return this.index++;
}
};
console.log(evil == 0 && evil == 1); // true
Now, this may be a fine parlor trick, but with the dark magic of Symbol.toPrimitive
we can go deeper and build a chameleon object that adapts to the shape of its surroundings.
let evil = {
[Symbol.toPrimitive](hint) {
switch (hint) {
case 'number':
return 333;
case 'string':
return 'boo';
case 'default':
return Date.now();
}
}
};
console.log(2 * evil); // 666
console.log((evil in {'boo': 1})); // true
console.log(evil == +new Date()); // true
with(evil) {}
with
is an ancient word of power. Deemed to be too error-prone by the elders and too hard to optimize by the engine implementers it has been banished from modern JavaScript. If you try to invoke it in a strict mode
function or a JavaScript module you will be met with a SyntaxError
. However, it is still possible for stict mode functions or modules to tap into its power by calling a function defined outside of strict mode.
with
adds a new object to the scope chain whose properties automatically become global variables that can be referred to without qualifiers.
let myContext = {
evil: 'demon',
macabre: 'ghost',
nocturnal: 'vampire',
};
with (myContext) {
console.log(evil); // 'demon'
console.log(macabre); // 'ghost'
}
console.log(nocturnal); // ReferenceError: nocturnal is not defined
with
can be used in a pinch for making a quick templating library.
function interpolate(context, template) {
with (context) {
return eval('`' + template + '`');
}
}
interpolate(
{ ghost: 'Slimer', place: 'hotel' },
'The ${ghost} haunted the ${place}.'
); // 'The Slimer haunted the hotel.'
But with
is usually best avoided because it is hard for a human reader to decide whether an unqualified name will be found along the scope chain, and if so, in which object.
function printGhost(ghost, graveyard) {
with (graveyard) {
console.log(ghost);
}
}
printGhost('Banquo', {}); // Banquo
printGhost('Banquo', { ghost: 'Brom Bones'}); // ???
Try to wrap your head around that, just don't lose it in the process.
The Haunted Annex
For our final story we will venture into my favorite section of the ECMAScript spec, the dreaded Annex B.
But first a test for those of you who have been studying. What doesn't the following code do? No cheating and typing it into a console. Try and reason through it on your own.
let a = 0;
a = a<!--10;
console.log(a); // ?
Before we learn the answer we must look back in time. Back to the earliest days of browser magic, back before the ECMAScript spec catalogued all of the arcane sorcery of the open web. The sorcerers at Netscape created JavaScript but the warlocks and witches of Microsoft were a jealous bunch. They coveted Netscape's power and sought to copy it for their own purposes. And copy they did, emulating it exactly down to the most minor spell and incantation.
Netscape was alarmed at this development and turned to the acolytes at ECMA for help. They wished to bind the magic of JavaScript into a specification to make it more predictable and useful to all. But when they started drafting the first spec Microsoft appeared and insisted the spec include every last minor spell that Microsoft had worked so hard to copy.
The sorcerers at Netscape did not like many of the minor spells they created in haste, but Microsoft insisted, until a compromise was reached. It was agreed to include these hacky cantrips, but hide them very back of the ledger. In a section called Annex B Additional ECMAScript Features for Web Browsers to dissuade all but the most curious from reading.
Annex B contains all of the dark and unsavory kludges the sorcerers at Netscape were too embarrassed to admit they unleashed upon the world but the witches and warlocks of Microsoft were too prideful at copying to let them forget.
Now with this history lesson over let us return to our test for it helps us to remember the context of the earliest JavaScript spells.
<script>
let a = 0;
a = a<!--10;
console.log(a); // 0
<!-- html comment -->
</script>
The old masters at Netscape had included <!--
in their grammar as an alias for the start of a comment block //
to facilitate html comments inside script tags. So our humble <!--
operator is a simple comment.
Sometimes the ghosts of the pasts can be scary, and other times they are just pragmatic.
Thanks to Ingrid for feedback and reading drafts and Mike Pennisi for sharing many of these spooky tales with me.
Photo credit: https://www.flickr.com/photos/9741459@N07/6522146791/