I fell in love with CoffeeScript a couple of years ago. Javascript has always seemed something of an interesting curiosity to me and I was happy to see the meteoric rise of Node.js, but coming from a background of Python I really preferred a cleaner syntax.
In any fast moving community it is inevitable that things will change, and so today we see a big shift toward ES6, the new version of Javascript. It incorporates a handful of the nicer features from CoffeeScript and is usable today through tools like Babel. Here are some of my thoughts and issues on moving away from CoffeeScript in favor of ES6.
While reading I suggest keeping open a tab to Babel's learning ES6 page. The examples there are great.
Holy punctuation, Batman! Say goodbye to your whitespace and hello to parenthesis, curly braces, and semicolons again. Even with the advanced ES6 syntax you'll find yourself writing a lot more punctuation. Accept it now. You'll see it in the examples below. The good news is that with a good linter the code will still be pretty.
I used to use CoffeeLint. Now I use ESLint through babel-eslint. Along with the Airbnb ES6 style guide it's quite strict. Your best bet is to set your editor up to lint as you type or on save. Atom's eslint plugin seems to work well and looks for a locally-installed/configured eslint per directory, which should find the .eslintrc
which you can copy from the Airbnb link above. Sublime has a similar plugin.
Since support for ES6 is still sketchy at best, you'll probably be transpiling just like you did with CoffeeScript. Unlike with CoffeeScript, there are some caveats, however.
-
Some ES6 features are just not available, like Proxies. Maybe some day we'll get the equivalent of Python's
__getattr__
and Ruby'smethod_missing
, but that day is not today. Them's the breaks. -
Some of the more interesting ES6 features require a polyfill/runtime. Symbols, generators, and new types like
WeakMap
for example. Examplepackage.json
snippet:{ ... "scripts": { "lint": "eslint --ext .js,.es6 src", "precompile": "npm run lint", "compile": "babel --optional runtime src --out-dir lib", }, "dependencies": { "babel-runtime": "^5.3.3" }, "devDependencies": { "babel": "^5.3.3", ... } ... }
Don't make the mistake of depending on
babel
because that will download many more packages than you need. Depend onbabel-runtime
, devDepend onbabel
for the build.By the way, these polyfills may have strange edge case behavior in some environments.
-
Some features are disabled by default. Specifically, those in TC39 Stage 0 like list comprehensions. This isn't Babel's fault - the specifications are not yet complete. Yes, we had these in CoffeeScript and now have to wait to use them again.
Haven't you heard the news? var
is out while her buddies let
and const
are in. With Javascript you should declare your variables. Default to const
and if a value needs to change, then use let
. The transpiler will enforce constants while the linter will complain if you use var
or nothing.
Keep in mind that const
applies only to the value itself. This takes a little getting used to:
const name = 'Daniel';
// This is a compile error
name = 'Kari';
// ---------
const options = {};
const items = [];
// This is *not* a compile error
options.foo = 'bar';
options.baz = 5;
items.push(1);
items.push(2);
// This *is* a compile error
options = {};
items = null;
Luckly, string interpolation is one area where you just need to retrain your fingers:
const name = 'World';
console.log(`Hello, ${name}!`);
Notice the backticks instead of single quotes.
It takes some time, so make sure your editor syntax highlights these properly or I guarantee you'll get stuff like #{name}
printed out by accident.
CoffeeScript has a concept of a range object that acts like an array of items from some starting point to some ending point. It's similar to Python's range()
built-in and is super useful for loops. ES6 doesn't have this, so you'll need to either use the new Array.from
and array.keys()
or make a custom range
function with your own loop logic.
let range;
# Coffee (inclusive): [0..10]
range = Array.from(new Array(11).keys());
# Coffee (exclusive): [0...10]
range = Array.from(new Array(10).keys());
# Coffee (non-zero): [2...10]
range = Array.from(new Array(10).keys()).slice(2);
# Coffee (decrementing): [10...2]
range = Array.from(new Array(10).keys()).slice(2).reverse();
# Then use it in a loop:
for (let i of range) {
console.log(i);
}
# Coffee: evens = (x for x in [0..10] by 2)
let evens = Array.from(new Array(6).keys()).map(i => i * 2);
Javascript has some new function types! With ES6 you can use both fat arrows and generators. Fat arrows behave like they do in CoffeeScript and keep this
passed through to the function body. They support both an implicit return shorthand and a long form that has no implied return value.
Splats are also supported, but the ellipsis is on the other side. Default arguments are also supported and work as you'd expect. Same with assignment destructuring.
CoffeeScript:
square = (value) -> value * value
someTask (err, result) =>
# Handle err and result
myFunc = ({source, flag}, args...) ->
otherFunc source, args...
Javascript:
const square = value => value * value;
someTask((err, result) => {
// Handle err and result
});
function myFunc({source, flag}, ...args) {
return otherFunc(source, ...args);
}
Generators provide a way to iterate through a large or possibly infinite list of items by only processing one at a time. Read up on them if you like. Here's a quick example:
// Instead of creating a 10000 item array, we yield each item as
// needed.
function *squares() {
for (let n = 0; n < 10000; n++) {
yield n * n;
}
}
for (let square of squares()) {
console.log(square);
}
The function *
syntax tells Babel that this is a generator, unlike in CoffeeScript where the presence of a yield
statement made it a generator. Note that generators can both yield and return values.
I'm happy to report that classes are actually very similar, and my main gripe with them is something that's being worked on in the spec and also has a usable workaround (only being able to create functions in a class). The following example will show just how similar the syntaxes are, including things like inheritance.
CoffeeScript:
class Account extends Foo
@types = ['checking', 'savings']
constructor: (@balance) ->
history: (done) ->
someLongTask (err, data) ->
# Do something with data
done null, data
deposit: (amount) ->
@balance += amount
Javascript:
class Account extends Foo {
constructor(balance) {
this.balance = balance;
}
history(done) {
someLongTask((err, data) => {
// Do something with data
done(null, data);
});
}
deposit(amount) {
this.balance += amount;
return this.balance;
}
}
// Currently, you can only declare functions in a class
Account.types = ['checking', 'savings'];
One cool feature is the ability to define getters
and setters
. Unfortunately they cannot be generator functions.
class Account {
constructor() {
this.balance = 0;
}
get underBudget() {
return this.balance >= 0;
}
get overBudget() {
return this.balance < 0;
}
}
const myAccount = Account();
myAccount.balance = 100;
console.log(myAccount.underBudget); // => true
Just like in CoffeeScript, ES6 classes can call on super
to access the parent's method of the same name. Unlike CS, though, you need to use the method name outside of the constructor. Notice the use of super.deposit
below:
class CachedAccount extends Account {
constructor() {
super();
this.cacheDirty = false;
}
deposit(amount) {
this.cacheDirty = true;
return super.deposit(amount);
}
}
Another neat feature is the ability to make iterable classes, and the ability to use generators for the iterator.
class MyIterable {
constructor(items) {
this.items = items;
}
*[Symbol.iterator]() {
for (let item of this.items) {
yield `Hello, ${item}`;
}
}
}
const test = new MyIterable([1, 2, 3, 4, 5]);
for (let item of test) {
console.log(item); // => Hello, 1...
}
ES6 brings with it a new module syntax. It'll take some getting used to, because it includes both a default export and named exports.
import _ from 'lodash';
import {item1, item2} from './mylib';
import * as library from 'library';
export const name = 'Daniel';
export function abc() {
return 1;
}
export class Toaster {
// ...
}
export default function def() {
return new Toaster();
}
Some interesting caveats:
-
If no default is given then you don't just get an object of all the named exports. Instead use
import * as moduleName from 'moduleName';
. Feels weird, right? Particularly for Python users, whereimport *
is a four letter word.// mymodule.js // ----------- export function yes() { return true; } // script-broken.js // ---------------- import mymodule from './mymodule'; // This gives an error about `undefined`! console.log(mymodule.yes()); // script-working.js // ----------------- import * as mymodule from './mymodule'; console.log(mymodule.yes());
-
If there is only one export and it is a default export, then
module.exports
will be set to it. If however there are other exports, you get something that's very strange to consume from normal Node.jsrequire
statements.// mymodule.js // ----------- export function yes() { return true; } function no() { return false; } export default {yes, no}; // script-working.js // ----------------- import mymodule, {yes} from './mymodule'; console.log(mymodule.no()); console.log(yes()); // script-broken.js // ---------------- const mymodule = require('./mymodule'); // Wat? This is an error. console.log(mymodule.no()); // This works instead. Turns out the module is an object with a 'default' // key that contains the default export. console.log(mymodule.default.no());
I currently don't have a great solution for this nonsense, but if you want to write libraries that are consumed by Node.js users who will likely be using
require
, this is something to keep in mind. As a possible workaround you can export default one thing and then assign all the things to it as properties. For example:// myclass.js // ---------- function export1() { return true; } function export2() { return false; } export default class MyClass { hello() { console.log('Hello!'); } } MyClass.export1 = export1; MyClass.export2 = export2; // script-working.js // ----------------- const MyClass = require('./myclass'); console.log(MyClass.export1()); // => 'true' const instance = new MyClass(); instance.hello(); // => 'Hello!'
Hopefully this article helps someone. It's been really fun learning ES6 and getting to play with all of the new toys like Babel and ESLint.
We've been migrating from CoffeeScript to ES6 at Bugsnag. We just open-sourced the tool we've been using to make it faster.
https://github.com/bugsnag/depercolator