Olov Lassus

ES3 <3 block scoped const and let => defs.js

04 May 2013

I released defs.js at the Front-Trends conference last week. Defs transpiles ES6 block scoped const and let declarations to beautiful ES3 code using vars. It’s also a pretty decent linter when it comes to variable and scope analysis.

ES6 block scoped let and const

Let me give you some background on why I created Defs. One of my favorite features in the upcoming ES6 language is block scoped let and const variable declarations (harmony wiki page). A while back I decided to start using them in my back-end code. This is possible thanks to node --harmony. Run it like so, make sure your code is in strict mode and off you go! With that a large JS annoyance (function scope) was gone with the wind, and I got to use const as a bonus - which turned out to work remarkably well with my JS style of programming. Nowadays mostly all of my node code is written in ES5 + cherry picked ES6 const and let features. Let’s call this constlet style JS for now.

constlet style JS is great, and I highly encourage you to try it out. It makes me much happier as a programmer and it makes my code much prettier and easier to reason about. Are you still reading? Up with a console and try it out in node! Then come back.

const is the norm

That block scoped variables is an awesome thing is no news for those of you that have a {} background. let is a better var in every respect so I won’t pitch it further. let is awesome. const, on the other hand, may seem unimpressive at a glance. If you’re not used to it then it may seem like something to use for the rare occasions when we define a numerical constant such as const godspeed = Infinity. But it’s so much more than that.

A striking thing that happens when you start using const is that you realize that the vast majority of your old vars were initialized but never modified (don’t confuse this with deep immutability which is a different story). Non-mutating variables are the norm, so they should have the default variable declaration keyword - which is const in ES6. For those variables that you modify, be it accumulators, loop indices or whatever - you make those stand out by using let. Once you get hooked on this style it’s impossible to go back. Ask any Scala-slinging friend of yours about val (~const) and var (~let). There are people swearing by this in Java-land too, finalista’s I’ve heard some of them call themselves because of the final keyword they have to add everywhere. To give you an idea of how common const is: The source code for defs.js has a const coverage of over 80%, which means that I used 4 consts for every let.

let is not the new var, const is.

Defs transpiles to beautiful ES3

So. Use constlet style JS and be happy, right? Except for the browser. We need to be able to run our client-side code in current browsers (so ES3 or ES5). Until now there was no decent way of transpiling constlet style JS back to ES3. Traceur and others use a style where a myriad of try-catch statements are generated which is as bad on your eyes as it is on your VM. This is no criticism on Traceur which is a very ambitious project with a lot of features to cover. Traceur will surely improve also when it comes to block scope.

So I created defs.js, a transpiler that takes constlet style JS and spits out plain-old ES3 code, as beautiful as its input and maximally non-intrusive. It works by doing static analysis, changing all const and let declarations to var, and an occasional variable rename here and there. The output looks more or less identical to the code you would have written with vars in the first place. Same line-numbers, easy debugging and no lock-in shall you decide to go back to the world of vars. Because Defs needs to do a bunch of scope analysis anyways, it ended up also being a pretty decent linter when it comes to variable and scope analysis.

Try Defs

Install Defs by npm install -g defs, then run defs mycode.js and there’s your transpiled code on stdout or your errors on stderr. There’s more info about configuration such as how to tell Defs about your globals (if you want it to analyse those) in the defs.js README.

Want to toy around directly? Bring up your browser’s JS console and copy and paste this: defs("const x = 1").src. It should give you "var x = 1" in return. Try defs("const x = 1; { let x = 2; }").src, which should return "var x = 1; { var x$0 = 2; }", a rename example. Try more advanced examples. Try modifying a const, try creating two consts (or lets) with the same name in the same block scope or try accessing a const or let before it has been initialized.

I know of only one use case that cannot be handled with renaming, namely loop closures - i.e. a closure that references a variable that is declared outside of its own scope but inside the loop. An example would be:

for (let x = 0; x < 10; x++) {
    let y = x; // we can skip this when v8 issue 2560 is closed
    arr.push(function() { return y; });
}

In this ES6 code let y is a new binding per iteration while a transformed var won’t be. If you run this through defs it will refuse to transpile it and give you this error:

line 3: can't transform closure. y is defined outside closure, inside loop

The solution is to do as we already to in our current ES3 or ES5 code, create the binding manually:

for (let x = 0; x < 10; x++) {
    (function(y) {
        arr.push(function() { return y; });
    })(x);
}

Defs will happily transpile that.

Toolchain and convenience

I can already hear people saying that they’d like to try this kind of thing but that the save-and-refresh development cycle is precious. Fear not. I guess that most aren’t running their code locally using file:// but use a local HTTP server, in which case you may have the opportunity of hooking a toolchain (including Defs) into it. If you serve using node this should be very simple.

It’s even simpler if you use Chrome. Go to chrome://flags, check Enable experimental JavaScript and you’ve got the same support for const and let as you have with node --harmony. Just remember that you must "use strict" for it to work.

Defs works great with James Halliday’s excellent Browserify. Here’s how I create the defs_bundle.js file that’s included in this page (view source):

browserify -r "./defs-main" > defs_bundle_constlet.js
# defs_bundle_constlet.js contains defs.js and all its dependencies

# defs-config.json:
# we allow duplicated vars because of defs dependencies
{
    "environments": ["browser"],
    "globals": {
        "define": false,
        "require": true
    },
    "disallowDuplicated": false
}

defs defs_bundle_constlet.js > defs_bundle.js

Things will be even easier once we have a Browserify plugin for Defs.

Underpinnings

Defs uses Ariya Hidayat’s Esprima parser to create an AST from source code. Defs performs static analysis on that AST and spits out errors or transformed source code. Static analysis is the hard part of Defs’ work, the actual source transformation is pretty trivial after that and it can be done in one fast textual shot using alter.js. If you wonder why I didn’t use JSShaper to create Defs then it’s for exactly that reason. Defs need a rock-solid and well-maintained parser. Esprima is that, my forked version of Narcissus I used for JSShaper is not. Thanks for Esprima Ariya, I have a couple of constlet parser bugs coming your way soon. :)

Scope of the Defs project

The scope [sic] of the Defs project is small on purpose. It transpiles const and let back to ES3 in a maximally non-intrusive manner, it performs static code analysis related to scope, and nothing else. Defs is not likely to take on other ES6 features.

Presentation

Feel free to check out my Front Trend presentation LET’s CONST together, right now (with ES3) for more info about Defs.

License

MIT because others are generous so I want to be generous too.

Try it, break it, help fix it don’t hate it

I hope that I triggered some interest in checking out what let and const could do for your code base. Use it with node --harmony and then consider using Defs to bring the same features to your client code. Check out the github project and npm package.

Let me know what works and what doesn’t. File issues or even better send pull requests. JS needs <3 and so does Defs. Peace and happy hacking! \o/

Show comments (reddit)


Follow me on Twitter

« Taking the leap    ↑ Home     »