The question is directed at people who have thought about code style in the context of the upcoming ECMAScript 6 (Harmony) and who have already worked with the language.
With () => {}
and function () {}
we are getting two very similar ways to write functions in ES6. In other languages lambda functions often distinguish themselves by being anonymous, but in ECMAScript any function can be anonymous. Each of the two types have unique usage domains (namely when this
needs to either be bound explicitly or explicitly not be bound). Between those domains there is a vast number of cases where either notation will do.
Arrow functions in ES6 have at least two limitations:
new
and cannot be used when creating prototype
this
bound to scope at initialisationThese two limitations aside, arrow functions could theoretically replace regular functions almost anywhere. What is the right approach using them in practice? Should arrow functions be used e.g.:
this
variable and we are not creating an object.What I am looking for is a guideline to selecting the appropriate function notation in the future version of ECMAScript. The guideline will need to be clear, so that it can be taught to developers in a team, and to be consistent so that it does not require constant refactoring back and forth from one function notation to another.
A while ago our team migrated all its code (a mid-sized AngularJS app) to JavaScript compiled using Traceur Babel. I'm now using the following rule of thumb for functions in ES6 and beyond:
function
in the global scope and for Object.prototype
properties.class
for object constructors.=>
everywhere else.Why use arrow functions almost everywhere?
thisObject
as the root. If even a single standard function callback is mixed in with a bunch of arrow functions there's a chance the scope will become messed up.function
immediately sticks out for defining the scope. A developer can always look up the next-higher function
statement to see what the thisObject
is.Why always use regular functions on the global scope or module scope?
thisObject
.window
object (global scope) is best addressed explicitly.Object.prototype
definitions live in the global scope (think String.prototype.truncate
etc.) and those generally have to be of type function
anyway. Consistently using function
on the global scope helps avoid errors.function foo(){}
than const foo = () => {}
— in particular outside other function calls. (2) The function name shows in stack traces. While it would be tedious to name every internal callback, naming all the public functions is probably a good idea.
Object constructors
Attempting to instantiate an arrow function throws an exception:
var x = () => {};
new x(); // TypeError: x is not a constructor
One key advantage of functions over arrow functions is therefore that functions double as object constructors:
function Person(name) {
this.name = name;
}
However, the functionally identical2 ES Harmony draft class definition is almost as compact:
class Person {
constructor(name) {
this.name = name;
}
}
I expect that use of the former notation will eventually be discouraged. The object constructor notation may still be used by some for simple anonymous object factories where objects are programmatically generated, but not for much else.
Where an object constructor is needed one should consider converting the function to a class
as shown above. The syntax works with anonymous functions/classes as well.
Readability of arrow functions
The probably best argument for sticking to regular functions - scope safety be damned - would be that arrow functions are less readable than regular functions. If your code is not functional in the first place, then arrow functions may not seem necessary, and when arrow functions are not used consistently they look ugly.
ECMAScript has changed quite a bit since ECMAScript 5.1 gave us the functional Array.forEach
, Array.map
and all of these functional programming features that have us use functions where for-loops would have been used before. Asynchronous JavaScript has taken off quite a bit. ES6 will also ship a Promise
object, which means even more anonymous functions. There is no going back for functional programming. In functional JavaScript, arrow functions are preferable over regular functions.
Take for instance this (particularly confusing) piece of code3:
function CommentController(articles) {
this.comments = [];
articles.getList()
.then(articles => Promise.all(articles.map(article => article.comments.getList())))
.then(commentLists => commentLists.reduce((a, b) => a.concat(b)));
.then(comments => {
this.comments = comments;
})
}
The same piece of code with regular functions:
function CommentController(articles) {
this.comments = [];
articles.getList()
.then(function (articles) {
return Promise.all(articles.map(function (article) {
return article.comments.getList();
}));
})
.then(function (commentLists) {
return commentLists.reduce(function (a, b) {
return a.concat(b);
});
})
.then(function (comments) {
this.comments = comments;
}.bind(this));
}
While any one of the arrow functions can be replaced by a standard function, there would be very little to gain from doing so. Which version is more readable? I would say the first one.
I think the question whether to use arrow functions or regular functions will become less relevant over time. Most functions will either become class methods, which make away with the function
keyword, or they will become classes. Functions will remain in use for patching classes through the Object.prototype
. In the mean time I suggest reserving the function
keyword for anything that should really be a class method or a class.
Notes
extend
keyword. A minor difference is that class declarations are constants, whereas function declarations are not.