What is the best way to add new features to a language? This blog post describes the approach taken by ECMAScript 6 [1], the next version of JavaScript. It is called One JavaScript, because it avoids versioning.
In principle, a new version of a language is a chance to clean it up, by removing outdated features or by changing how features work. That means that new code doesn’t work in older implementations of the language and that old code doesn’t work in a new implementation. Each piece of code is linked to a specific version of the language. Two approaches are common for dealing with versions being different.
First, you can take an “all or nothing” approach and demand that, if a code base wants to use the new version, it must be upgraded completely. Python took that approach when upgrading from Python 2 to Python 3. A problem with it is that it may not be feasible to migrate all of an existing code base at once, especially if it is large. Furthermore, the approach is not an option for the web, where you’ll always have old code and where JavaScript engines are updated automatically.
Second, you can permit a code base to contain code in multiple versions, by tagging code with versions. On the web, you could tag ECMAScript 6 code via a dedicated Internet media type. Such a media type can be associated with a file via an HTTP header:
Content-Type: application/ecmascript;version=6
It can also be associated via the type
attribute of the <script>
element (whose default value is text/javascript
):
<script type="application/ecmascript;version=6">
···
</script>
This specifies the version out of band, externally to the actual content. Another option is to specify the version inside the content (in-band). For example, by starting a file with the following line:
use version 6;
Both ways of tagging are problematic: out-of-band versions are brittle and can get lost, in-band versions add clutter to code.
A more fundamental issue is that allowing multiple versions per code base effectively forks a language into sub-languages that have to be maintained in parallel. This causes problems:
Therefore, versioning is something to avoid, especially for JavaScript and the web.
But how can we get rid of versioning? By always being backwards-compatible. That means we must give up some of our ambitions w.r.t. cleaning up JavaScript:
As a consequence, no versions are needed for new engines, because they can still run all old code. David Herman calls this approach to avoiding versioning One JavaScript (1JS) [2], because it avoids splitting up JavaScript into different versions or modes. As we shall see later, 1JS even undoes some of a split that already exists, due to strict mode.
Supporting new code on old engines is more complicated. You have to detect in the engine what version of the language it supports. If it doesn’t support the latest version, you have to load different code: your new code compiled to an older version. That is how you can already use ECMAScript 6 in current engines: you compile it to ECMAScript 5 [1:1]. Apart from performing the compilation step ahead of time, you also have the option of compiling in the engine, at runtime.
Detecting versions is difficult, because many engines support parts of versions before they support them completely. For example, this is how you’d check whether an engine supports ECMAScript 6’s for-of
loop – but that may well be the only ES6 feature it supports:
function isForOfSupported() {
try {
eval("for (var e of ['a']) {}");
return true;
} catch (e) {
// Possibly: check if e instanceof SyntaxError
}
return false;
}
Mark Miller describes how the Caja library detects whether an engine supports ECMAScript 5. He expects detection of ECMAScript 6 to work similarly, eventually.
One JavaScript does not mean that you have to completely give up on cleaning up the language. Instead of cleaning up existing features, you introduce new, clean, features. One example for that is let
, which declares block-scoped variables and is an improved version of var
. It does not, however, replace var
, it exists alongside it, as the superior option.
One day, it may even be possible to eliminate features that nobody uses, anymore. Some of the ES6 features were designed by surveying JavaScript code on the web. Two examples (that are explained in more detail later) are:
let
is available in non-strict mode, because let[x]
rarely appears on the web.Strict mode was introduced in ECMAScript 5 to clean up the language. It is switched on by putting the following line first in a file or in a function:
'use strict';
Strict mode introduces three kinds of breaking changes:
with
statement is forbidden. It lets users add arbitrary objects to the chain of variable scopes, which slows down execution and makes it tricky to figure out what a variable refers to.implements interface let package private protected public static yield
ReferenceError
. In sloppy mode, a global variable is created in this case.TypeError
. In non-strict mode, it simply has no effect.arguments
doesn’t track the current values of parameters, anymore.this
is undefined
in non-method functions. In sloppy mode, it refers to the global object (window
), which meant that global variables were created if you called a constructor without new
.Strict mode is a good example of why versioning is tricky: Even though it enables a cleaner version of JavaScript, its adoption is still relatively low. The main reasons are that it breaks some existing code, can slow down execution and is a hassle to add to files (let alone interactive command lines). I love the idea of strict mode and don’t nearly use it often enough.
One JavaScript means that we can’t give up on sloppy mode: it will continue to be around (e.g. in HTML attributes). Therefore, we can’t build ECMAScript 6 on top of strict mode, we must add its features to both strict mode and non-strict mode (a.k.a. sloppy mode). Otherwise, strict mode would be a different version of the language and we’d be back to versioning. Unfortunately, two ECMAScript 6 features are difficult to add to sloppy mode: let
declarations and block-level function declarations. Let’s examine why that is and how to add them, anyway.
let
declarations in sloppy mode let
enables you to declare block-scoped variables. It is difficult to add to sloppy mode, because let
is only a reserved word in strict mode. That is, the following two statements are legal in ECMAScript 5 sloppy mode:
var let = [];
let[0] = 'abc';
In strict ECMAScript 6, you get an exception in line 1, because you are using the reserved word let
as a variable name. And the statement in line 2 is interpreted as a let
variable declaration.
In sloppy ECMAScript 6, the first line does not cause an exception, but the second line is still interpreted as a let
declaration. The pattern in that line is rare enough that ES6 can afford to make this interpretation. Other ways of writing let
declarations can’t be mistaken for existing sloppy syntax:
let foo = 123;
let {x,y} = computeCoordinates();
ECMAScript 5 strict mode forbids function declarations in blocks. The specification allowed them in sloppy mode, but didn’t specify how they should behave. Hence, various implementations of JavaScript support them, but handle them differently.
ECMAScript 6 wants a function declaration in a block to be local to that block. That is OK as an extension of ES5 strict mode, but breaks some sloppy code. Therefore, ES6 provides “web legacy compatibility semantics” for browsers that lets function declarations in blocks exist at function scope.
The identifiers yield
and static
are only reserved in ES5 strict mode. ECMAScript 6 uses context-specific syntax rules to make them work in sloppy mode:
yield
is only a reserved word inside a generator function.static
is currently only used inside class literals, which are implicitly strict (see below).The bodies of modules and classes are implicitly in strict mode in ECMAScript 6 – there is no need for the 'use strict'
marker. Given that virtually all of our code will live in modules in the future, ECMAScript 6 effectively upgrades the whole language to strict mode.
The bodies of other constructs (such as arrow functions and generator functions) could have been made implicitly strict, too. But given how small these constructs usually are, using them in sloppy mode would have resulted in code that is fragmented between the two modes. Classes and especially modules are large enough to make fragmentation less of an issue.
It is interesting to note that, inside a <script>
element, you can’t declaratively import modules via an import
statement. Instead, there will be a new element, which may be called <module>
, whose insides are much like a module [3]: Modules can be imported asynchronously and code is implicitly strict and not in global scope (variables declared at the top level are not global).
Another way of importing a module, that works inside both elements, is the programmatic System.import()
API that returns a module asynchronously, via a promise.
The downside of One JavaScript is that you can’t fix existing quirks, especially the following two.
First, typeof null
should return the string 'null'
and not 'object'
. But fixing that would break existing code. On the other hand, adding new results for new kinds of operands is OK, because current JavaScript engines already occasionally return custom values for host objects. One example are ECMAScript 6’s symbols:
> typeof Symbol.iterator
'symbol'
Second, the global object (window
in browsers) shouldn’t be in the scope chain of variables. But it is also much too late to change that now. At least you won’t be in global scope in modules and within <module>
elements.
One JavaScript means making ECMAScript 6 completely backwards compatible. It is great that that succeeded. Especially appreciated is that modules (and thus most of our code) are implicitly in strict mode.
In the short term, adding ES6 constructs to both strict mode and sloppy mode is more work when it comes to writing the language specification and to implementing it in engines. In the long term, both the spec and engines profit from the language not being forked (less bloat etc.). Programmers profit immediately from One JavaScript, because it makes it easier to get started with ECMAScript 6.
Using ECMAScript 6 today (overview plus links to more in-depth material) ↩︎ ↩︎
The original 1JS proposal (warning: out of date): “ES6 doesn’t need opt-in” by David Herman. ↩︎