When should I use arrow functions in ECMAScript 6?
Solution 1:
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:
- Use
function
in the global scope and forObject.prototype
properties. - Use
class
for object constructors. - Use
=>
everywhere else.
Why use arrow functions almost everywhere?
- Scope safety: When arrow functions are used consistently, everything is guaranteed to use the same
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. - Compactness: Arrow functions are easier to read and write. (This may seem opinionated so I will give a few examples further on.)
- Clarity: When almost everything is an arrow function, any regular
function
immediately sticks out for defining the scope. A developer can always look up the next-higherfunction
statement to see what thethisObject
is.
Why always use regular functions on the global scope or module scope?
- To indicate a function that should not access the
thisObject
. - The
window
object (global scope) is best addressed explicitly. - Many
Object.prototype
definitions live in the global scope (thinkString.prototype.truncate
, etc.) and those generally have to be of typefunction
anyway. Consistently usingfunction
on the global scope helps avoid errors. - Many functions in the global scope are object constructors for old-style class definitions.
- Functions can be named1. This has two benefits: (1) It is less awkward to write
function foo(){}
thanconst 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. - Function declarations are hoisted, (meaning they can be accessed before they are declared), which is a useful attribute in a static utility function.
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 ECMAScript 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
- Named arrow functions have been deferred in the ES6 specification. They might still be added a future version.
- According to the draft specification, "Class declarations/expressions create a constructor function/prototype pair exactly as for function declarations" as long as a class does not use the
extend
keyword. A minor difference is that class declarations are constants, whereas function declarations are not. - Note on blocks in single statement arrow functions: I like to use a block wherever an arrow function is called for the side effect alone (e.g., assignment). That way it is clear that the return value can be discarded.
Solution 2:
According to the proposal, arrows aimed "to address and resolve several common pain points of traditional function expressions". They intended to improve matters by binding this
lexically and offering terse syntax.
However,
- One cannot consistently bind
this
lexically - Arrow function syntax is delicate and ambiguous
Therefore, 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 third 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:
- Mocha (~120k downloads yesterday) exposes methods for its tests via
this
. - Grunt (~63k downloads yesterday) exposes methods for build tasks via
this
. - Backbone (~22k downloads yesterday) defines methods accessing
this
. - Event APIs (like the DOM's) refer to an
EventTarget
withthis
. -
Prototypal APIs that are patched or extended refer to instances with
this
.
(Statistics 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. According 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 parentheses
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:
Guideline for Function Notation in ES6:
- Always create procedures with
function
. - Always assign
this
to a variable. Do not use() => {}
.
Solution 3:
Arrow functions were created to simplify function scope
and solving the this
keyword by making it simpler. They utilize the =>
syntax, which looks like an arrow.
Note: It does not replace the existing functions. If you replace every function syntax with arrow functions, it's not going to work in all cases.
Let's have a look at the existing ES5 syntax. If the this
keyword were inside an object’s method (a function that belongs to an object), what would it refer to?
var Actor = {
name: 'RajiniKanth',
getName: function() {
console.log(this.name);
}
};
Actor.getName();
The above snippet would refer to an object
and print out the name "RajiniKanth"
. Let's explore the below snippet and see what would this point out here.
var Actor = {
name: 'RajiniKanth',
movies: ['Kabali', 'Sivaji', 'Baba'],
showMovies: function() {
this.movies.forEach(function(movie) {
alert(this.name + " has acted in " + movie);
});
}
};
Actor.showMovies();
Now what about if the this
keyword were inside of method’s function
?
Here this would refer to window object
than the inner function
as its fallen out of scope
. Because this
, always references the owner of the function it is in, for this case — since it is now out of scope — the window/global object.
When it is inside of an object
’s method — the function
’s owner is the object. Thus the this keyword is bound to the object. Yet, when it is inside of a function, either stand alone or within another method, it will always refer to the window/global
object.
var fn = function(){
alert(this);
}
fn(); // [object Window]
There are ways to solve this problem in our ES5 itself. Let us look into that before diving into ES6 arrow functions on how solve it.
Typically you would, create a variable outside of the method’s inner function. Now the ‘forEach’
method gains access to this
and thus the object’s
properties and their values.
var Actor = {
name: 'RajiniKanth',
movies: ['Kabali', 'Sivaji', 'Baba'],
showMovies: function() {
var _this = this;
this.movies.forEach(function(movie) {
alert(_this.name + " has acted in " + movie);
});
}
};
Actor.showMovies();
Using bind
to attach the this
keyword that refers to the method to the method’s inner function
.
var Actor = {
name: 'RajiniKanth',
movies: ['Kabali', 'Sivaji', 'Baba'],
showMovies: function() {
this.movies.forEach(function(movie) {
alert(this.name + " has acted in " + movie);
}.bind(this));
}
};
Actor.showMovies();
Now with the ES6 arrow function, we can deal with lexical scoping issue in a simpler way.
var Actor = {
name: 'RajiniKanth',
movies: ['Kabali', 'Sivaji', 'Baba'],
showMovies: function() {
this.movies.forEach((movie) => {
alert(this.name + " has acted in " + movie);
});
}
};
Actor.showMovies();
Arrow functions are more like function statements, except that they bind the this to the parent scope. If the arrow function is in the top scope, the this
argument will refer to the window/global scope, while an arrow function inside a regular function will have its this argument the same as its outer function.
With arrow functions this
is bound to the enclosing scope at creation time and cannot be changed. The new operator, bind, call, and apply have no effect on this.
var asyncFunction = (param, callback) => {
window.setTimeout(() => {
callback(param);
}, 1);
};
// With a traditional function if we don't control
// the context then can we lose control of `this`.
var o = {
doSomething: function () {
// Here we pass `o` into the async function,
// expecting it back as `param`
asyncFunction(o, function (param) {
// We made a mistake of thinking `this` is
// the instance of `o`.
console.log('param === this?', param === this);
});
}
};
o.doSomething(); // param === this? false
In the above example, we lost the control of this. We can solve the above example by using a variable reference of this
or using bind
. With ES6, it becomes easier in managing the this
as its bound to lexical scoping.
var asyncFunction = (param, callback) => {
window.setTimeout(() => {
callback(param);
}, 1);
};
var o = {
doSomething: function () {
// Here we pass `o` into the async function,
// expecting it back as `param`.
//
// Because this arrow function is created within
// the scope of `doSomething` it is bound to this
// lexical scope.
asyncFunction(o, (param) => {
console.log('param === this?', param === this);
});
}
};
o.doSomething(); // param === this? true
When not to use arrow functions
Inside an object literal.
var Actor = {
name: 'RajiniKanth',
movies: ['Kabali', 'Sivaji', 'Baba'],
getName: () => {
alert(this.name);
}
};
Actor.getName();
Actor.getName
is defined with an arrow function, but on invocation it alerts undefined because this.name
is undefined
as the context remains to window
.
It happens because the arrow function binds the context lexically with the window object
... i.e., the outer scope. Executing this.name
is equivalent to window.name
, which is undefined.
Object prototype
The same rule applies when defining methods on a prototype object
. Instead of using an arrow function for defining sayCatName method, which brings an incorrect context window
:
function Actor(name) {
this.name = name;
}
Actor.prototype.getName = () => {
console.log(this === window); // => true
return this.name;
};
var act = new Actor('RajiniKanth');
act.getName(); // => undefined
Invoking constructors
this
in a construction invocation is the newly created object. When executing new Fn(), the context of the constructor Fn
is a new object: this instanceof Fn === true
.
this
is setup from the enclosing context, i.e., the outer scope which makes it not assigned to newly created object.
var Message = (text) => {
this.text = text;
};
// Throws "TypeError: Message is not a constructor"
var helloMessage = new Message('Hello World!');
Callback with dynamic context
Arrow function binds the context
statically on declaration and is not possible to make it dynamic. Attaching event listeners to DOM elements is a common task in client side programming. An event triggers the handler function with this as the target element.
var button = document.getElementById('myButton');
button.addEventListener('click', () => {
console.log(this === window); // => true
this.innerHTML = 'Clicked button';
});
this
is window in an arrow function that is defined in the global context. When a click event happens, the browser tries to invoke the handler function with button context, but arrow function does not change its pre-defined context. this.innerHTML
is equivalent to window.innerHTML
and has no sense.
You have to apply a function expression, which allows to change this depending on the target element:
var button = document.getElementById('myButton');
button.addEventListener('click', function() {
console.log(this === button); // => true
this.innerHTML = 'Clicked button';
});
When user clicks the button, this
in the handler function is the button. Thus this.innerHTML = 'Clicked button'
correctly modifies the button text to reflect the clicked status.
References
- When 'Not' to Use Arrow Functions