They tried to cover this up. In designing ECMAScript 2015 (a.k.a. ES6, a.k.a. ES2015), the authors identified a number of undesirable side effects of their work. “Why worry?” they asked. “People will be so smitten with arrow functions and block-scope bindings that they won’t care about a few measly backwards-breaking changes.” Well I care, and I have evidence that suggests I’m not alone.
Through an extensive survey of JavaScript across the web, I have found a file that demonstrates these hazardous changes to the language. This code, published in 2009 to a pop singer’s fan site, describes a program that is 100% compliant with the ECMAScript 5 standard. The web site will not function as intended for much longer–indeed, many of today’s JavaScript engines have already implemented new language features that interfere with its original behavior.
Needless to say, this is the least glamorous aspect of the new language, and I’m sure there are plenty of people who would rather it go unnoticed. Publicizing this information won’t earn me any friends, but I trust that history will judge me favorably.
I’ll include the source in its entirety then step through piece-by-piece to call out the increasingly-broken aspects. Finally (and most importantly) I’ll share some thoughts on what all this means for the web as a platform.
The File
$(document).ready(function() {
// set the date for midnight according to the user's clock
var LastSongRelease = new Date("2010-03-31T00:00:00");
LastSongRelease.setTime(
LastSongRelease.getTimezoneOffset() * 60 * 1000 + LastSongRelease
);
// "let" for "L.ast Song E.xclamation T.ext"
var let = [];
var first = 0, second = 1, third = 2;
{
let[first] = "omg it's out";
let[second] = "in theaters now";
let[third] = "the wait is over";
}
let = let.join("?").toUpperCase().split("?");
var SlideShow = Object.getOwnPropertyDescriptor({
get ctor() {
var $slides = $("#slides .miley-photo");
var current = 0;
this.go = function(change) {
current = (current + change) % $slides.length;
for (var i = 0 in $slides.toArray()) {
if (i === current === delete function() {}.length) {
$slides.eq(i).hide();
} else {
$slides.eq(i).show();
}
}
};
}
}, "ctor").get;
var slideShow = new SlideShow();
$("div#next-pic").bind("click", function() { slideShow.go(1); });
$("div#prev-pic").bind("click", function() { slideShow.go(-1); });
// update the countdown timer
setInterval(function() {
var milliseconds = LastSongRelease - Date.now();
var oldContent = $("#countdown").html();
var newContent = milliseconds < Number.prototype ?
"<b>" + let[-milliseconds % 3] + "!!!</b>" :
milliseconds + " milliseconds until <i>The Last Song</i> comes out!";
// only update if the text has changed
var updateDetector = "({ get " + JSON.stringify(oldContent) +
"() {}, get " + JSON.stringify(newContent) + "() {} })";
try {
eval(updateDetector);
} catch(err) {
var err;
if (!(err instanceof SyntaxError)) {
throw err;
}
return;
}
$("#countdown").html(newContent);
}, 500);
});
Explanations
It’s just 63 lines of code, but I count nine distinct features that will break in ES6. Let’s get started.
// set the date for midnight according to the user's clock
var LastSongRelease = new Date("2010-03-31T00:00:00");
LastSongRelease.setTime(
LastSongRelease.getTimezoneOffset() * 60 * 1000 + LastSongRelease
);
Here, a Date instance is being created from a ISO 8601-formatted string. Crucially, no time zone offset is specified. According to ES5, “The value of an absent time zone offset is ‘Z’.” That’s why the author needed to explicitly correct for the local clock as configured on the user’s computer. ES6, on the other hand, states, “If the time zone offset is absent, the date-time is interpreted as a local time.” In compliant engines, the “correction” is unnecessary, making the target time incorrect. This means any Date initialized to compensate for the user’s local time zone in this way will be wrong.
var let = [];
var first = 0, second = 1, third = 2;
{
let[first] = "omg it's out";
let[second] = "in theaters now";
let[third] = "the wait is over";
}
In ES5, let
is a valid identifier outside of strict
mode. While it remains a valid identifier in
ES6,
the new destructuring
assignment
syntax takes precedence here. ES6 interprets the token sequence let [
as the
beginning of a destructuring assignment. Instead of defining numeric properties
on the array stored in let
, the final three statements will define new
block-scoped bindings for first
, second
, and third
(using the values
"o"
, "i"
, and "t"
respectively).
let = let.join("?").toUpperCase().split("?");
Note the use of a Deseret character (specifically: lower case “long I”) as a
string separator. In order to restore the original array, this snippet relies
on the fact that ES5’s String.prototype.toUpperCase
does not honor surrogate
pairs, so the transformed string still
contains the same Deseret characters. ES6, on the other hand, requires that
implementors derive the result according to case
mappings.
The transformed string will no longer contain any instances of lower case “long
I”, and the resultant array will only have a single element: a string
representing the original three values joined by an upper case version of the
separator.
I’ll admit: the Deseret lower case “long I” is a somewhat uncommon choice for a separator, but the implications of this change are no less disturbing.
var SlideShow = Object.getOwnPropertyDescriptor({
get ctor() {
// (we'll get to this code in a moment)
}
}, "ctor").get;
var slideShow = new SlideShow();
In ES5, functions declared as “accessor properties” like these are just normal
functions. In ES6, they are special “method”
functions
that do not define a prototype and cannot be used as a constructor. The
definition of SlideShow
is not in itself threatened, but ES6 environments
will generate an error when that function is invoked with new
.
That’s right, all code written in this way will suddenly produce new runtime errors! Sure, I’ve never in my life seen a constructor defined like this, but who am I to judge?
for (var i = 0 in slides.toArray()) {
// (we'll get to this code in a moment)
}
Although ES5 allows for an Initialiser to be used
here, the semantics of the for..in
statement
make it completely unnecessary. ES6 defines the grammar more strictly, so the
same code will be rejected by compliant
parsers.
Picture the surprise of the author of this code when this stops working! Well,
okay, with this one they will probably be more surprised that it worked in the
first place. There’s more, though!
if (i === current === delete function() {}.length) {
$slides.eq(i).hide();
} else {
$slides.eq(i).show();
}
ES5 states that the length
property of Function instances is not
configurable, so the delete
operation returns
false
and the branch is only executed when i
and current
are not
strictly equal. That property is in fact configurable in
ES6,
so the behavior of this code is inverted–the “current” image will be hidden,
and all others will be displayed.
Across the web, code that uses the expression delete function() {}.length
instead of the literal false
will suddenly start behaving in surprising ways.
This honestly seemed more catastrophic when I first started writing this post.
var newContent = milliseconds < Number.prototype ?
"<b>" + let[-milliseconds % 3] + "!!!</b>" :
milliseconds + " milliseconds until <i>The Last Song</i> comes out!";
This code depends on the fact that ES5 defines the Number prototype as “a
Number object […] whose value is +0”. Useful
as that may be, it is no longer true in ES6. The spec is very clear on this:
“The Number prototype object is an ordinary object. It is not a Number
instance”.
In an ES6 engine, the condition in the above ternary will always evaluate to
false
, meaning (now that The Last Song has been released) the countdown
timer will be updated with an increasingly large negative value.
Can you imagine the confusion of all those visitors when the countdown starts
counting negative time? All because the developer innocently used
Number.prototype
instead of… well, instead of 0
, which I suppose might be
the more natural/common choice here.
// only update if the text has changed
var updateDetector = "({ get " + JSON.stringify(oldContent) +
"() {}, get " + JSON.stringify(newContent) + "() {} })";
try {
eval(updateDetector);
} catch(err) {
// (we'll get to this code in a moment)
}
While ES5 explicitly forbids duplicate property names in object initializers, ES6 makes no such restriction.
Now here is a truly hazardous change! I mean, is it really wise to break the untold millions of scripts that compare strings by evaluating carefully-escaped representations inserted into an object literal definition and dynamically evaluated, branching on the presence of a SyntaxError?
Written out like that, this incompatibility doesn’t seem so bad. Let’s just forget this one.
try {
// (see above)
} catch(err) {
var err;
if (!(err instanceof SyntaxError)) {
throw err;
}
return;
}
The semantics for catch
blocks in ECMAScript
5 are a little tricky. ES5 first copies the
surrounding lexical environment, and it then inserts a new binding for the
identifier specified within the parenthesis that follow the catch
token.
Notably, ES5 makes no special restrictions on variable declarations within the
catch
block itself. ES6, on the other hand, explicitly forbids re-defining
the binding for the “caught”
error.
This snippet, which runs just fine in Internet Explorer 8, will not parse in
ES6-compliant engines.
It probably goes without saying that the var
declaration is totally
unnecessary in this case. I’m going to level with you: I’m having second
thoughts about the severity of all these conflicts.
Reflections
Okay, so maybe I laid it on a bit thick in the intro. Given how bizarre most of these examples are, it’s difficult to argue for the severity of any of the incompatibilities. And I don’t mean “bizarre” in the hoity-toity “I’m a professional developer with standards” sense, either. I mean it in the completely sincere, “I didn’t know some of those things were even possible” sense.
To be clear: the Nightmarefile doesn’t demonstrate all the backwards-breaking changes in ES6. The spec helpfully documents every intentional incompatibility under sections labeled Annex D and Annex E. I think you’ll agree that the other incompatibilities are even more esoteric (and consequently less troubling) than those described above. Annex D and E omit the rationale for the changes (one document can only have so much information, after all), so you may find yourself asking, “Why?” Lucky for us, TC39’s decision making process is publicly documented, so you can usually find rationale for any given design decision (it just might take some digging). For insight into the process, check out the TC39 Meeting Minutes, the ECMAScript bug tracker and the ES-Discuss mailing list archives.
Considering the breadth of the changes, it’s actually amazing that so little has been broken. This is no accident; early in the discussion for ES6, TC39 member Dave Herman coined the term “One JavaScript” to signify the committee’s aversion to “breaking the web.” Every new feature has been vetted against the ES5 specification, and as you can see, only in the rarest of circumstances has existing code been threatened. No news is good news in this case, and TC39 deserves recognition for the extra care this detail requires.