var, let, and the one I still use
The loop bug let fixes, why const isn't what its name suggests, and a build step on a site that didn't have one.
The spec is meant to be finalized in a couple of months and I’ve been writing bits of it for about half a year, which is a strange way to adopt a language version and apparently just how we do things now.
The mechanism is a transpiler. You write the new syntax, a tool rewrites it into the old syntax, browsers get something they already understand. Babel is the one I’m using, which was called 6to5 until a couple of months ago and got renamed because it turned out not to be about 6-to-5 specifically.
What this means practically is that ES6 isn’t a thing you turn on. It’s about 20 separate features arriving at once, each with its own case, and you pick. So here’s where I’ve landed on the ones I actually use, after a couple of projects.
Let (this bug haunt me no more)
var is function-scoped, which everyone knows and almost nobody’s intuition accounts for. The classic demonstration:
for (var i = 0; i < buttons.length; i++) {
buttons[i].addEventListener('click', function () {
console.log(i); // always 3
});
}
for (let i = 0; i < buttons.length; i++) {
buttons[i].addEventListener('click', function () {
console.log(i); // 0, 1, 2
});
}
There’s one i in the first version, shared by all three handlers, and by the time anyone clicks the loop has finished and it’s 3. The workaround has always been an IIFE to make a new scope, which works and which I’ve written a hundred times and which requires you to know about this in advance.
let is block-scoped, so each iteration gets its own. That’s it, that’s the fix, and it turns a thing you have to know into a thing you don’t.
This one I’d adopt without hesitating because it’s a gotcha that’s not gonna getcha no more.
The ill-named const
const means the binding can’t be reassigned. It does not mean the value is immutable, and the name really implies otherwise.
const config = { retries: 3 };
config.retries = 5; // okay
config = {}; // TypeError
Which took me a while to stop finding annoying. But the thing that won me over is that when I’m reading code, const tells me this name will mean the same thing for the whole block, and that’s most of what I want to know when I’m scanning. The mutation question is separate and usually visible nearby.
So I’ve ended up on const by default, let when something genuinely changes, and var basically never.
Template literals and arrow functions
Template literals are backticks with ${} interpolation, and they’re the least interesting feature I use constantly. String concatenation with + is fine right up until you’re building HTML, at which point it becomes an unreadable pile of quotes and pluses, and this fixes that. They’re also multiline, which removes another class of ugly.
Arrow functions I’m more mixed on. The short form is nicer for callbacks:
var names = people.map(function (p) { return p.name; });
const names = people.map(p => p.name);
But the part that actually matters isn’t brevity, it’s that arrows don’t have their own this. They take it from the surrounding scope. Which means the var self = this line at the top of every constructor goes away, and so does .bind(this) on every handler.
That’s a genuine improvement and also the reason I’d say be careful, because it means arrows and function are not interchangeable. Using an arrow as an object method or a constructor does not do what you want, and it’s easy to forget about that.
Is it worth it?
Here’s the thing that nags. To write this I’ve added a build step to projects that didn’t have one.
For a site that was three script tags and no tooling, that’s a real cost. There’s now a thing that has to run before the code works, a node_modules folder, a config file, and a failure mode where someone clones the repo and nothing happens until they know the incantation. And the transpiled output is bigger than what I wrote, sometimes noticeably, because a for-of loop turns into quite a lot of machinery.
The counter is that most projects have a build step anyway for SCSS, so it’s a new task rather than a new category of thing. While that’s true of nearly everything I build, it isn’t universally true and I’ve seen this argument get waved away too fast.
I've added a build step to projects that didn't have one in order to write a version of the language no browser here runs yet.
The other counter, and this is the one I find more persuasive, is that this is temporary in a way that most build steps aren’t. Autoprefixer is forever, because new prefixes keep arriving. Transpiling ES6 ends. At some point the browsers I support all speak it and the tool comes out, and the code I wrote is just the code. That’s a nice property and I can’t think of many tools that have it.
My recommendation
If you’ve got a build step already, let and const and template literals cost you nothing and are strictly better. I’d take those today.
Arrow functions, yes, but understand the this thing before you convert anything over.
Modules I’m leaving alone for now. The syntax is beautiful but the story for actually loading them is a mess involving another tool entirely, and I’d rather wait than adopt two new things at once to solve a problem I don’t have.
And if you don’t have a build step and the project is small and it works: I don’t think you’re missing much yet. I’d probably hold out. The language will come to you eventually, which is an unusual thing to be able to say about a dependency.
Six months in, that’s where I’m at. Ask me again after the spec actually lands 🙂