Arrow Functions

We’ve touched on this binding complications with functions earlier in this chapter, and they’re covered at length in the this & Object Prototypes title of this series. It’s important to understand the frustrations that this-based programming with normal functions brings, because that is the primary motivation for the new ES6 => arrow function feature.

Let’s first illustrate what an arrow function looks like, as compared to normal functions:

  1. function foo(x,y) {
  2. return x + y;
  3. }
  4. // versus
  5. var foo = (x,y) => x + y;

The arrow function definition consists of a parameter list (of zero or more parameters, and surrounding ( .. ) if there’s not exactly one parameter), followed by the => marker, followed by a function body.

So, in the previous snippet, the arrow function is just the (x,y) => x + y part, and that function reference happens to be assigned to the variable foo.

The body only needs to be enclosed by { .. } if there’s more than one expression, or if the body consists of a non-expression statement. If there’s only one expression, and you omit the surrounding { .. }, there’s an implied return in front of the expression, as illustrated in the previous snippet.

Here’s some other arrow function variations to consider:

  1. var f1 = () => 12;
  2. var f2 = x => x * 2;
  3. var f3 = (x,y) => {
  4. var z = x * 2 + y;
  5. y++;
  6. x *= 3;
  7. return (x + y + z) / 2;
  8. };

Arrow functions are always function expressions; there is no arrow function declaration. It also should be clear that they are anonymous function expressions — they have no named reference for the purposes of recursion or event binding/unbinding — though “Function Names” in Chapter 7 will describe ES6’s function name inference rules for debugging purposes.

Note: All the capabilities of normal function parameters are available to arrow functions, including default values, destructuring, rest parameters, and so on.

Arrow functions have a nice, shorter syntax, which makes them on the surface very attractive for writing terser code. Indeed, nearly all literature on ES6 (other than the titles in this series) seems to immediately and exclusively adopt the arrow function as “the new function.”

It is telling that nearly all examples in discussion of arrow functions are short single statement utilities, such as those passed as callbacks to various utilities. For example:

  1. var a = [1,2,3,4,5];
  2. a = a.map( v => v * 2 );
  3. console.log( a ); // [2,4,6,8,10]

In those cases, where you have such inline function expressions, and they fit the pattern of computing a quick calculation in a single statement and returning that result, arrow functions indeed look to be an attractive and lightweight alternative to the more verbose function keyword and syntax.

Most people tend to ooh and aah at nice terse examples like that, as I imagine you just did!

However, I would caution you that it would seem to me somewhat a misapplication of this feature to use arrow function syntax with otherwise normal, multistatement functions, especially those that would otherwise be naturally expressed as function declarations.

Recall the dollabillsyall(..) string literal tag function from earlier in this chapter — let’s change it to use => syntax:

  1. var dollabillsyall = (strings, ...values) =>
  2. strings.reduce( (s,v,idx) => {
  3. if (idx > 0) {
  4. if (typeof values[idx-1] == "number") {
  5. // look, also using interpolated
  6. // string literals!
  7. s += `$${values[idx-1].toFixed( 2 )}`;
  8. }
  9. else {
  10. s += values[idx-1];
  11. }
  12. }
  13. return s + v;
  14. }, "" );

In this example, the only modifications I made were the removal of function, return, and some { .. }, and then the insertion of => and a var. Is this a significant improvement in the readability of the code? Meh.

I’d actually argue that the lack of return and outer { .. } partially obscures the fact that the reduce(..) call is the only statement in the dollabillsyall(..) function and that its result is the intended result of the call. Also, the trained eye that is so used to hunting for the word function in code to find scope boundaries now needs to look for the => marker, which can definitely be harder to find in the thick of the code.

While not a hard-and-fast rule, I’d say that the readability gains from => arrow function conversion are inversely proportional to the length of the function being converted. The longer the function, the less => helps; the shorter the function, the more => can shine.

I think it’s probably more sensible and reasonable to adopt => for the places in code where you do need short inline function expressions, but leave your normal-length main functions as is.

Not Just Shorter Syntax, But this

Most of the popular attention toward => has been on saving those precious keystrokes by dropping function, return, and { .. } from your code.

But there’s a big detail we’ve skipped over so far. I said at the beginning of the section that => functions are closely related to this binding behavior. In fact, => arrow functions are primarily designed to alter this behavior in a specific way, solving a particular and common pain point with this-aware coding.

The saving of keystrokes is a red herring, a misleading sideshow at best.

Let’s revisit another example from earlier in this chapter:

  1. var controller = {
  2. makeRequest: function(..){
  3. var self = this;
  4. btn.addEventListener( "click", function(){
  5. // ..
  6. self.makeRequest(..);
  7. }, false );
  8. }
  9. };

We used the var self = this hack, and then referenced self.makeRequest(..), because inside the callback function we’re passing to addEventListener(..), the this binding will not be the same as it is in makeRequest(..) itself. In other words, because this bindings are dynamic, we fall back to the predictability of lexical scope via the self variable.

Herein we finally can see the primary design characteristic of => arrow functions. Inside arrow functions, the this binding is not dynamic, but is instead lexical. In the previous snippet, if we used an arrow function for the callback, this will be predictably what we wanted it to be.

Consider:

  1. var controller = {
  2. makeRequest: function(..){
  3. btn.addEventListener( "click", () => {
  4. // ..
  5. this.makeRequest(..);
  6. }, false );
  7. }
  8. };

Lexical this in the arrow function callback in the previous snippet now points to the same value as in the enclosing makeRequest(..) function. In other words, => is a syntactic stand-in for var self = this.

In cases where var self = this (or, alternatively, a function .bind(this) call) would normally be helpful, => arrow functions are a nicer alternative operating on the same principle. Sounds great, right?

Not quite so simple.

If => replaces var self = this or .bind(this) and it helps, guess what happens if you use => with a this-aware function that doesn’t need var self = this to work? You might be able to guess that it’s going to mess things up. Yeah.

Consider:

  1. var controller = {
  2. makeRequest: (..) => {
  3. // ..
  4. this.helper(..);
  5. },
  6. helper: (..) => {
  7. // ..
  8. }
  9. };
  10. controller.makeRequest(..);

Although we invoke as controller.makeRequest(..), the this.helper reference fails, because this here doesn’t point to controller as it normally would. Where does it point? It lexically inherits this from the surrounding scope. In this previous snippet, that’s the global scope, where this points to the global object. Ugh.

In addition to lexical this, arrow functions also have lexical arguments — they don’t have their own arguments array but instead inherit from their parent — as well as lexical super and new.target (see “Classes” in Chapter 3).

So now we can conclude a more nuanced set of rules for when => is appropriate and not:

  • If you have a short, single-statement inline function expression, where the only statement is a return of some computed value, and that function doesn’t already make a this reference inside it, and there’s no self-reference (recursion, event binding/unbinding), and you don’t reasonably expect the function to ever be that way, you can probably safely refactor it to be an => arrow function.
  • If you have an inner function expression that’s relying on a var self = this hack or a .bind(this) call on it in the enclosing function to ensure proper this binding, that inner function expression can probably safely become an => arrow function.
  • If you have an inner function expression that’s relying on something like var args = Array.prototype.slice.call(arguments) in the enclosing function to make a lexical copy of arguments, that inner function expression can probably safely become an => arrow function.
  • For everything else — normal function declarations, longer multistatement function expressions, functions that need a lexical name identifier self-reference (recursion, etc.), and any other function that doesn’t fit the previous characteristics — you should probably avoid => function syntax.

Bottom line: => is about lexical binding of this, arguments, and super. These are intentional features designed to fix some common problems, not bugs, quirks, or mistakes in ES6.

Don’t believe any hype that => is primarily, or even mostly, about fewer keystrokes. Whether you save keystrokes or waste them, you should know exactly what you are intentionally doing with every character typed.

Tip: If you have a function that for any of these articulated reasons is not a good match for an => arrow function, but it’s being declared as part of an object literal, recall from “Concise Methods” earlier in this chapter that there’s another option for shorter function syntax.

If you prefer a visual decision chart for how/why to pick an arrow function:

Arrow Functions - 图1