One JavaScript: avoiding versioning in ECMAScript 6

[2014-12-18] esnext, dev, javascript
(Ad, please don’t block)

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.

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:

  • Engines become bloated, because they need to implement the semantics of all versions. The same applies to tools analyzing the language (e.g. style checkers such es JSLint).
  • Programmers need to remember how the versions differ.
  • Code becomes harder to refactor, because you need to take versions into consideration when you move pieces of code.

Therefore, versioning is something to avoid, especially for JavaScript and the web.

Evolution without versioning  

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:

  • We can’t introduce breaking changes: Being backwards-compatible means not removing features and not changing features. The slogan for this principle is: “don’t break the web”.
  • We can, however, add new features and make existing features more powerful.

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.
  • Function declarations do occasionally appear in non-strict blocks, which is why the ES6 specification describes measures that web browsers can take to ensure that such code doesn’t break.

Strict mode and ECMAScript 6  

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:

  • Syntactic changes: some previously legal syntax is forbidden in strict mode. For example:
    • The 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.
    • Deleting an unqualified identifier (a variable, not a property) is forbidden.
    • Functions can only be declared at the top level of a scope.
    • More identifiers are reserved: implements interface let package private protected public static yield
  • More errors. For example:
    • Assigning to an undeclared variable causes a ReferenceError. In sloppy mode, a global variable is created in this case.
    • Changing read-only properties (such as the length of a string) causes a TypeError. In non-strict mode, it simply has no effect.
  • Different semantics: Some constructs behave differently in strict mode. For example:
    • 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.

Supporting sloppy mode  

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();

Block-level function declarations in sloppy mode  

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.

Other keywords  

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:

  • 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).

Implicit strict mode  

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.

Things that can’t be fixed  

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.

Conclusion  

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.

Further reading  


  1. Using ECMAScript 6 today (overview plus links to more in-depth material) ↩︎ ↩︎

  2. The original 1JS proposal (warning: out of date): “ES6 doesn’t need opt-in” by David Herman. ↩︎

  3. ECMAScript 6 modules in future browsers ↩︎