Introduction to ES6, Part 3: Modules, Generators, and Using ES6 Today
Modules
One of the areas JavaScript has particularly struggled in is in that there’s never been a module system native to the language. There have been implementations like browser 5, which let us use the common jest specification in the browser. There are libraries like RequireJS, which is the AND specification to give us the module system in the browser. These things work pretty well, but they’ve all had drawbacks because they haven’t really been implemented natively.
When Node.js really became popular and it brought with it the common JS syntax(e.g. the module.exports and var x = require x syntax), it was clear that these syntax worked really well and this has been well embraced. I find that module system in Node really good, and no doubt it kind of led the inspiration for developing a module system in the browser too.
Modules in ES6 takes a similar-ish approach to Node. You can tell that one was inspired by the other but there are some pretty big differences. It also makes heavy use of the destructuring that we saw earlier and you’ll recognize the syntax from that.
Named Exports
I have two files, app.js and foo.js:
app.js
var foo = 2;
var bar = 3;
export {foo, bar};
foo.js
import {foo} from ‘app’
console.log(foo); // 2
In app.js I’ve two variables: foo and bar. In the bottom I’m using the export keyword to export foo and bar. These are what we call a named export. App.js has two named exports: one is called foo, the other is called bar. We can also go this kind of in line, so back here I have “var foo = 2” and “var bar = 3” and I’m going to use the export {} syntax.
If you want to just create a variable and export it straight away, you can chuck the export onto the beginning of the line like:
export var foo = 2;
which will have the same affect.
And you can see down at that bottom in foo.js I’m importing it, and again using the curly bracket syntax.
foo.js
import {foo} from ‘app’
cosole.log(foo); // 2
So here I’m saying in foo.js you can read this as import the named export foo from app. And you don’t need the .js extension on the end, that’s added for you. And I can log foo and I can get to it.
Default Exports
Along with named exports, modules can also have what’s called a default export.
app.js
export default function() {
return 2;
};
foo.js
import foo from ‘app’
console.log(foo()); // 2
In this example, I’m exporting a default export in app.js, which in this case happens to be a function (it could just be a number or an object, anything at all that returns to you). Inside foo.js I can import it, but the crucial difference here is I don’t use the curly brackets around foo. This is because I’m not getting a named export—I’m just getting the default export for app.js, which means I can name the function in whatever I want to.
App.js could export both default and named exports. Imagine utility libraries like underscore or low dash. They might have a default export, which is the entire underscore object, but they might have named exports for every single one of the functions they provide. So it’s perfectly fine for things to have a default export and a bunch of named exports too.
There are also different syntaxes, but I’m not entirely sure if this has survived the spec. However, this lets you kind of get at all the named exports in one go.
app.js
export var foo = 2;
export var bar = 3;
foo.js
module stuff from ‘app’;
stuff.foo // 2
stuff.bar // 3
Generators
One of the things that’s coming as well is generators. Generators provide a way of almost working infinitely so we can calculate massive numbers, say like the two thousandth of a number because of the way it generates this work. Generators let you jump through each step to generate these big numbers.
What I’m more excited about is generators potentially let us work with asynchronous code as if it were synchronous.
Callback Hell
This is a typical example from Node.js land where I’m using some module to connect to my database and query for things:
Person.findOne({id: 5}, (per) =>
{
// person has been got
Location.findOne(…, (log) => {
// location has been got
});
});
In this example, I’m finding a person with an id of 5 and I’ve got a callback function “Person”. Once I’ve got that person, I’d use location.find0ne to find their location, and that has a callback that takes me to the location.
This quickly gets into nesting, which is usually known as callback hell, where your code will quickly veer off to the right-hand side of the screen. Promises are one way to solve callback hell, but I’m not actually going to cover them today.
Asynchronous Code in Generators
Generators are another potential way to solve callback hell when working with asynchronous code.
var per = yield Person.findOne(…);
var loc = yield location.findOne(…);
// async but reads as sync !
So this doesn’t work out of the box, as the library that you’re using needs to have support for this. However, if you’re using a library that does support this, we can instead use the yield keyword and what you can see here is I’ve suddenly got what reads as synchronous code because it is line after line. The yield keyword is basically yielding to Person.findOne and location.findOne and just waiting for them to respond. When they respond, it comes back and assigns the right data to the person.
Synchronous code is much easier to reason about. When you’ve got five lines and you know that they happen one after the other when the previous line is finished executing, that’s much easier to reason about when you’ve got five nested callback levels deep of stuff going on. So I’m really interested to see how that develops. I think a lot of libraries will support this in the future.
ES6 Today
I could go on for hours and hours about all the other features in ES6, but in terms of using this stuff today, I really want to encourage you to go and play with this.
I’m going to talk about solutions that let you use all this stuff today in the browsers and in actual production apps, and it works really well. Where I work, we have an app that runs ES6. It uses the ES6 modules, arrow functions, spread operators, destructuring, template string, a whole load of stuff, and we just run it through a tool that converts it into JavaScript that will run in today’s browsers.
The ES6 Compatibility Table
A person called Kangax on Github maintains a compatibility table that shows you all the features of ES6, and how they are supported in both browsers like Chrome, Firefox, IE and so on, but also in tools like Traceur and others which I’ll mention in a minute. This is the best place to go and see where the features are and what’s implemented in what browsers.
You can also head into Chrome and go to Flags and look for the Enable JavaScript Harmony hashtag and that will turn on a bunch of features in Chrome not in the main default implementation yet. That lets you play with a lot of things in the Chrome developer console.
Traceur
There’s also tools that effectively transpile your JavaScript from ES6 down into ES5. They take JavaScript written that ES6 uses like classes, destructuring, arrow functions, whatever it may be and compiles that into code that doesn’t use that stuff. It basically uses JavaScript that is natively implemented across all the main browsers today so you can write in your editor ES6 and run in a browser ES5.
Traceur is one such tool. It’s backed by Google. Traceur is a tool that takes your input file and spits out a new file with it all converted into ES5.
Node —harmony
If you run Node, you can use the -–harmony flag to get at a load a lot of ES6 features that aren’t quite as stable yet.
io.js
If you have moved onto io.js, the framework by default supports more ES6. So if you work the on server side where you want to potentially use this stuff, take a look at io.js I haven’t used it so I can’t vouch for it, but one of the first things they did when they split off from Node was upgrade the version of web kit it uses. They kind of gained a bunch ES6 features by doing that.
Babel
There’s also Babel, which was known previously as 6to5.js. Babel is like Traceur, where it’s another tool that takes ES6 code and converts it to ES5 code. The main difference between them is that Traceur doesn’t care about the redevelopment output. If you give Traceur some ES6, the output’s probably going to be kind of mangled. You’re probably not going to be able to read through it.
Bable, on the other hand, tries to make the output code as close as possible to the code you would have written in real life, so it’s a bit easier to read and to follow through with. I think their feature support is mostly equivalent. There’s probably a couple of where one supports a feature that the other doesn’t, but they’re mostly on the same kind of level.
System JS
In terms of the ES6 modules, the best way to get started is with this tool, which is made by Guy Bedford. SystemJS is a universal module loader for JavaScript. That means it can take modules written in ES6 syntaxes (the new syntax I showed you earlier: AND, which is the syntax RequireJS uses, and CommonJSs which is the syntax Node uses), and load them all in the browser and makes it all work. It has a bunch of very clever code that I could never hope to understand that makes all that possible.
Basically, we write all our code using ES6 modules and SystemJS is responsible for poly-fitting in new features that will be in ES6 but aren’t yet to make all this stuff work in the browser today. You also install the ES6 module loader, which is a dependency of SystemJS, and you can have all your ES6 modules loaded in as well. So it works really nicely and I would recommend using it if you’re starting a new JavaScript project. SystemJS comes with support for Babel as well as Traceur. You can kind of pick whichever one you prefer and it will work with both.
New projects should use ES6 modules
It sounds almost too good to be true how it works, but it really does work seamlessly and nicely.
The spec for ES6 modules is finalized. The import and export syntax isn’t going to change, so now is a really good time to pick this up. Use SystemJS to make ES6 modules work. When this stuff is natively implemented into the browsers you need to support, you can just remove that entire dependency, and everything else will work.
In terms of compiling to ES5, we use Traceur, which takes all those module syntaxes and compiles it into a new syntax that uses an API called system.register and then system.import.
There are two parts of the ES6 module spec: the import and export syntax, and this new loader API that I very briefly mentioned. This is what you call system.import in JavaScript to load in an additional module. When Traceur compiles ES6 module code down, it uses another API called system.register, which lets you register a module. It can just go and grab that function and execute that. System.register and system.import aren’t supported yet in browsers.
They will be down the line, as will all the ES6 stuff, but what SystemJS does is it provides those for us. It kind of poly-fills this system.register and system.import commands, so you do still need to have SystemJS hanging around with the compiled Traceur codes I believe.
All in all, part of SystemJS is just to poly-fill this new API that doesn’t exist yet, but will.
- Part 1: Arrow functions, Classes & Object Literals
- Part 2: Template Strings, Destructuring, Function Arguments & Scope
- Q&A: Debugging Workflow, ES6 vs. TypeScript, and More
About Jack Franklin
Jack Franklin is the author of the book Beginning jQuery. Jack was also asked by Addy Osmani to contribute to his Backbone Fundamentals book (the chapter on using Backbone and RequireJS specifically.) Jack runs the popular JavaScript Playground blog, on which he writes tutorials about a variety of JavaScript topics including ES6, GoldNote and more. He is currently a developer over at GoCardless and is a regular writer for DotNet, which is the largest web development magazine over in the UK. He wrote the JavaScript Gallery section for a number of months on JavaScript topics, including a recent cover feature on JavaScript libraries.