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.
According to the proposal, arrows aimed "to address and resolve several common pain points of traditional Function Expression
." They intended to improve matters by binding this
lexically and offering terse syntax.
However,
this
lexicallyTherefore, arrow functions create opportunities for confusion and errors, and should be excluded from a JavaScript programmer's vocabulary, replaced with function
exclusively.
Regarding lexical this
this
is problematic:
function Book(settings) {
this.settings = settings;
this.pages = this.createPages();
}
Book.prototype.render = function () {
this.pages.forEach(function (page) {
page.draw(this.settings);
}, this);
};
Arrow functions intend to fix the problem where we need to access a property of this
inside a callback. There are already several ways to do that: One could assign this
to a variable, use bind
, or use the 3rd argument available on the Array
aggregate methods. Yet arrows seem to be the simplest workaround, so the method could be refactored like this:
this.pages.forEach(page => page.draw(this.settings));
However, consider if the code used a library like jQuery, whose methods bind this
specially. Now, there are two this
values to deal with:
Book.prototype.render = function () {
var book = this;
this.$pages.each(function (index) {
var $page = $(this);
book.draw(book.currentPage + index, $page);
});
};
We must use function
in order for each
to bind this
dynamically. We can't use an arrow function here.
Dealing with multiple this
values can also be confusing, because it's hard to know which this
an author was talking about:
function Reader() {
this.book.on('change', function () {
this.reformat();
});
}
Did the author actually intend to call Book.prototype.reformat
? Or did he forget to bind this
, and intend to call Reader.prototype.reformat
? If we change the handler to an arrow function, we will similarly wonder if the author wanted the dynamic this
, yet chose an arrow because it fit on one line:
function Reader() {
this.book.on('change', () => this.reformat());
}
One may pose: "Is it exceptional that arrows could sometimes be the wrong function to use? Perhaps if we only rarely need dynamic this
values, then it would still be okay to use arrows most of the time."
But ask yourself this: "Would it be 'worth it' to debug code and find that the result of an error was brought upon by an 'edge case?'" I'd prefer to avoid trouble not just most of the time, but 100% of the time.
There is a better way: Always use function
(so this
can always be dynamically bound), and always reference this
via a variable. Variables are lexical and assume many names. Assigning this
to a variable will make your intentions clear:
function Reader() {
var reader = this;
reader.book.on('change', function () {
var book = this;
book.reformat();
reader.reformat();
});
}
Furthermore, always assigning this
to a variable (even when there is a single this
or no other functions) ensures one's intentions remain clear even after the code is changed.
Also, dynamic this
is hardly exceptional. jQuery is used on over 50 million websites (as of this writing in February 2016). Here are other APIs binding this
dynamically:
this
.this
.this
.EventTarget
with this
.this
.(Stats via http://trends.builtwith.com/javascript/jQuery and https://www.npmjs.com.)
You are likely to require dynamic this
bindings already.
A lexical this
is sometimes expected, but sometimes not; just as a dynamic this
is sometimes expected, but sometimes not. Thankfully, there is a better way, which always produces and communicates the expected binding.
Regarding terse syntax
Arrow functions succeeded in providing a "shorter syntactical form" for functions. But will these shorter functions make you more successful?
Is x => x * x
"easier to read" than function (x) { return x * x; }
? Maybe it is, because it's more likely to produce a single, short line of code. Accoring to Dyson's The influence of reading speed and line length on the effectiveness of reading from screen,
A medium line length (55 characters per line) appears to support effective reading at normal and fast speeds. This produced the highest level of comprehension . . .
Similar justifications are made for the conditional (ternary) operator, and for single-line if
statements.
However, are you really writing the simple mathematical functions advertised in the proposal? My domains are not mathematical, so my subroutines are rarely so elegant. Rather, I commonly see arrow functions break a column limit, and wrap to another line due to the editor or style guide, which nullifies "readability" by Dyson's definition.
One might pose, "How about just using the short version for short functions, when possible?" But now a stylistic rule contradicts a language constraint: "Try to use the shortest function notation possible, keeping in mind that sometimes only the longest notation will bind this
as expected." Such conflation makes arrows particularly prone to misuse.
There are numerous issues with arrow function syntax:
const a = x =>
doSomething(x);
const b = x =>
doSomething(x);
doSomethingElse(x);
Both of these functions are syntactically valid. But doSomethingElse(x);
is not in the body of b
, it is just a poorly-indented, top-level statement.
When expanding to the block form, there is no longer an implicit return
, which one could forget to restore. But the expression may only have been intended to produce a side-effect, so who knows if an explicit return
will be necessary going forward?
const create = () => User.create();
const create = () => {
let user;
User.create().then(result => {
user = result;
return sendEmail();
}).then(() => user);
};
const create = () => {
let user;
return User.create().then(result => {
user = result;
return sendEmail();
}).then(() => user);
};
What may be intended as a rest parameter can be parsed as the spread operator:
processData(data, ...results => {}) // Spread
processData(data, (...results) => {}) // Rest
Assignment can be confused with default arguments:
const a = 1;
let x;
const b = x => {}; // No default
const b = x = a => {}; // "Adding a default" instead creates a double assignment
const b = (x = a) => {}; // Remember to add parens
Blocks look like objects:
(id) => id // Returns `id`
(id) => {name: id} // Returns `undefined` (it's a labeled statement)
(id) => ({name: id}) // Returns an object
What does this mean?
() => {}
Did the author intend to create a no-op, or a function that returns an empty object? (With this in mind, should we ever place {
after =>
? Should we restrict ourselves to the expression syntax only? That would further reduce arrows' frequency.)
=>
looks like <=
and >=
:
x => 1 ? 2 : 3
x <= 1 ? 2 : 3
if (x => 1) {}
if (x >= 1) {}
To invoke an arrow function expression immediately, one must place ()
on the outside, yet placing ()
on the inside is valid and could be intentional.
(() => doSomething()()) // Creates function calling value of `doSomething()`
(() => doSomething())() // Calls the arrow function
Although, if one writes (() => doSomething()());
with the intention of writing an immediately-invoked function expression, simply nothing will happen.
It's hard to argue that arrow functions are "more understandable" with all the above cases in mind. One could learn all the special rules required to utilize this syntax. Is it really worth it?
The syntax of function
is unexceptionally generalized. To use function
exclusively means the language itself prevents one from writing confusing code. To write procedures that should be syntactically understood in all cases, I choose function
.
Regarding a guideline
You request a guideline that needs to be "clear" and "consistent." Using arrow functions will eventually result in syntactically-valid, logically-invalid code, with both function forms intertwined, meaningfully and arbitrarily. Therefore, I offer the following:
function
.this
to a variable. Do not use () => {}
.