What is the best way to add new features to a language? This chapter describes the approach taken by ECMAScript 6. It is called One JavaScript, because it avoids versioning.
let
declarations in sloppy modeIn 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:
It can also be associated via the type
attribute of the <script>
element (whose default value is text/javascript
):
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:
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 backward-compatible. That means we must give up some of our ambitions w.r.t. cleaning up JavaScript: We can’t introduce breaking changes. Being backward-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) [1], 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.
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 are:
let
-declarations are difficult to add to non-strict mode, because let
is not a reserved word in that mode. The only variant of let
that looks like valid ES5 code is:
Research yielded that no code on the web uses a variable let
in non-strict mode in this manner. That enabled TC39 to add let
to non-strict mode. Details of how this was done are described later in this chapter.
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:
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 non-strict 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 non-strict 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 ES5 sloppy code:
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 (that uses destructuring).
In sloppy ECMAScript 6, the first line does not cause an exception, but the second line is still interpreted as a let
declaration. This way of using the identifier let
is so rare on the web that ES6 can afford to make this interpretation. Other ways of writing let
declarations can’t be mistaken for sloppy ES5 syntax:
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 considering 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.
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'
. TC39 tried fixing it, but it broke 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:
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, one won’t be in global scope in modules and let
never creates properties of the global object, not even when used in global scope.
ECMAScript 6 does introduce a few minor breaking changes (nothing you’re likely to encounter). They are listed in two annexes:
One JavaScript means making ECMAScript 6 completely backward-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.
[1] The original 1JS proposal (warning: out of date): “ES6 doesn’t need opt-in” by David Herman.