ES6 doesn't need opt-in

David Herman dherman at
Sat Dec 31 17:46:40 PST 2011

Happy new year, everyone. I've got some good news.

There's been grumbling lately about the ES6 version opt-in [1]. The detractors are not right about everything, but it's true that both the MIME type opt-in:

    <script type="application/ecmascript;version=6">

and the pragma opt-in:

    use version 6;

are painful -- ugly, verbose, and a permanent tax for opting to use the new semantics.

We can do better, and the way we can do it is with modules. We can introduce module syntax in a backwards-compatible way, and use them as the implicit opt-in for any new semantics [2].

And the benefit we get is **just one JavaScript**.

# Backwards-compatible syntax

Both `use` and `module` can be contextual keywords, with a restricted production that disallows a newline after the keyword. To avoid ambiguity with contextual infix operators like `is` and `isnt` [3], such as:

    module is {}

we can resolve in favor of the declaration form in statement context, and in any other expression contexts `module` is just an ordinary identifier.

This is totally backwards-compatible: all the new forms would have been parse errors in ES5:

    module foo { }
    module bar {
    module {

and in all cases that ASI accepts in ES5, it continues to parse the same, i.e., not as a module:

    module // semicolon inserted
    foo    // semicolon inserted

# Modules as the opt-in for new semantics

Any ES6 feature that breaks web-compatibility can be made available only within module code. For example, if `let` as a keyword breaks the web, then in global code we treat it as an identifier, but we treat it as a keyword once you're inside a module. Similarly, the proper but backwards-incompatible scoping semantics for block-local functions would only be enabled within module code.

We could do the same for `yield`, but since it's only meaningful within function*, we should allow generator functions outside of modules as well. I'm not sure whether we can/should reserve `yield` as a keyword in global code.

Each new feature can be considered independently. Anything that is only allowed within modules becomes a carrot to lead programmers to the improved semantics, but whenever we can compatibly make something available to global code, we should do so. No point needlessly depriving programmers of ES6 goodies such as destructuring or spread/rest.

# What about eliminating the window from the scope chain?

Short answer: giving up.

You can still do it with a custom loader, and I still want to push on new HTML <meta> conveniences for specifying a custom loader to use for the remainder of the program. In that context you could create a loader with a nice, clean global scope.

# But then what about static scoping?

Compile-time checking of variables is a really important goal, and we don't have to abandon it. We just do the checking only within module code, relative to the current contents of the global object. This means it's still technically possible for globals to disappear at runtime (and consequently throw errors).

But this doesn't bother me. First of all, variable errors due to dynamic deletion of globals are not the common case. Second, you can avoid globals entirely by strictly importing from modules. Finally, you can easily create loaders with frozen global objects. An environment like SES might choose to do this. But we retain compatibility in the default browser context.

If you ask me, this compromise is **totally** worth it.

# Anonymous modules as a simple way of opting in

A common pattern on the web is to protect yourself against creating unnecessary globals is to wrap top-level code in an IIFE:

    (function() {
        /* program goes here */

By allowing anonymous modules, we simultaneously make this more idiomatic and implicitly opt in to all the goodies of ES6:

    module {
        /* program goes here */

Kills two birds with one stone!

# Avoiding rightward drift

Still, whether you use an IIFE or an anonymous module, the extra rightward drift (requiring an initial level of indentation around the entire program body) is annoying. A simple pragma takes care of that:

    // equivalent to wrapping program in module { ... }
    use module;

This is not strictly necessary but avoiding an extra level indentation in every single program is a big win when you multiply it over every program ever written, and some editors will fight you if you try not to indent within curlies. But notice that even though this is a kind of opt-in pragma, it still is strictly *less* code than you have to write today with an IIFE -- kills two birds with a smaller stone than ES5 requires to kill one.

# Summing up

We can make ES6 100% backwards-compatible, using modules to opt in to backwards-incompatible changes. It replaces the notion of a new language "mode" with the simple syntactic characteristic of whether the code is in a module. And you can now mix old code and new code freely, even within the same <script> tag or source file.

One final note: the "...;version=6" MIME-type could be used to hide ES6 scripts from downrev browsers. So it may still have a place, but not for opting in to new semantics.



[2] We can't be future-compatible, as some have hoped to accomplish with HTML, except maybe with macros. We'll get there if/when we can.


More information about the es-discuss mailing list