Get your brains ready folks, this is a mental adventure that you don’t want to miss!
Stabby functions, also known by their more boring official name Arrow Functions, were introduced to help eliminate this
hacks. What are this
hacks?
I think we’ve all written code like the type below, and wondered why the interpreter puked up “Uncaught TypeError: Cannot read property ‘appendChild’ of undefined:”
function Astronauts(root, astronautObjs, nationality) { this.root = root; this.astronautObjs = astronautObjs; this.nationality = nationality; } Astronauts.prototype.show = function() { this.astronautObjs.forEach(function(astronaut) { var li = document.createElement("li"); li.innerHTML = astronaut.name + " (" + this.nationality + ")"; this.root.appendChild(li); }); }
Of course the reason is that in the forEach
callback, this
does not refer to the instance of Astronauts
that called show()
. Instead, it is the global/window object.
To work around the error, there’s a couple of idioms that JS developers use. First, the me/_this/that hack:
function Astronauts(root, astronautObjs, nationality) { this.root = root; this.astronautObjs = astronautObjs; this.nationality = nationality; } Astronauts.prototype.show = function() { var me = this; this.astronautObjs.forEach(function(astronaut) { var li = document.createElement("li"); li.innerHTML = astronaut.name + " (" + me.nationality + ")"; me.root.appendChild(li); }); }
The name that JS developers use varies, but I like me
because it’s short. In this idiom, the value of this
from the enclosing context is passed to the forEach
callback by assigning it to me
, which is closed over by the anonymous function passed to the forEach
. Now the callback can use properties from the this
that it expects.
The second idiom is the bind(this) hack:
Astronauts.prototype.show = function() { var appendName = function(astronaut) { var li = document.createElement("li"); li.innerHTML = astronaut.name + " (" + this.nationality + ")"; this.root.appendChild(li); }.bind(this); this.names.forEach(appendName); }
Here, the value of this
from the enclosing context is preserved by passing the enclosing context’s this
value to bind()
, which sets the function’s context to the enclosing context’s this
value.
Reasonable people may disagree on whether these are hacks, or that a language feature is needed to eliminate these idioms. Personally, I’ve encountered this issue more than I’d care to admit and am glad to see it addressed in ES2015.
Down the rabbit hole
If you are relatively new to JS like myself, right now you may be thinking “WTF is going on?” What is this
? When does the value change? How does it just appear in functions? What is bind
doing?
Since this post is about arrow functions, which have different rules for this when compared to regular functions, I’ll just cover the basics that are needed to understand what is different in arrow functions. To fill in the gaps, I recommend going through the Udacity Object Oriented Javascript course.
So basically, this
in a regular function contains the value of the object that invoked the function. A decent rule of thumb is that you can find what the value of this
will be by looking at the code that called the function. Inside the callee function, this
will have the value of the object to the left of the dot at run time in the caller function.
function Astronauts(root, astronautObjs, nationality) { this.root = root; this.astronautObjs = astronautObjs; this.nationality = nationality; } Astronauts.prototype.show = function() { console.log(this); var me = this; this.astronautObjs.forEach(function(astronaut) { var li = document.createElement("li"); li.innerHTML = astronaut.name + " (" + me.nationality + ")"; me.root.appendChild(li); }); } var ul = document.createElement("ul"); var astronautsUS = new Astronauts(ul, [{name: "Alan Shepard"}, {name: "Gus Grissom"}], "American"); // show() will log astronautsUS astronautsUS.show();
Note that this
is always determined at run time, never at write time.
// show() will log the window/global object, because when the function is invoked // without an explicit caller, the object that invokes the function is the // window/global object. Also throws an "Uncaught TypeError" when execution // reaches the forEach, because the window/global object doesn't have an // astronautObjs property. var show = astronautsUS.show; show();
bind
can be used to pin a this
value, overriding the default behavior:
// just like calling astronautsUS.show() // showBind() will log astronautsUS var showBind = show.bind(astronautsUS); showBind();
Back to Arrow Functions
Here’s that code written using an arrow function in the callback instead of a regular function. This shows the main differences between the two types of functions.
Astronauts.prototype.show = function() { console.log(this); this.astronautObjs.forEach(astronaut => { var li = document.createElement("li"); li.innerHTML = astronaut.name + " (" + this.nationality + ")"; this.root.appendChild(li); }); }
The first difference is major. In the callback, this
no longer refers to the (window/global) object that invoked the function, i.e. it no longer refers to the function’s context. Instead, this
is lexically scoped, meaning that the value of this
in the callback is determined by the value of this
in the enclosing scope. Therefore, the callback no longer requires the use of var me = this;
to use astronautsUS.nationality
and astronautsUS.root
. Instead, it simply uses this
.
The second difference, which is minor in my opinion, is that the syntax is more terse. The function
keyword has been eliminated, as has the parentheses around the single parameter. The terseness really is apparent when writing single-line functions, though again reasonable people may disagree on which is more readable:
// these two functions do the same thing var mulBy2 = function(a) { return a * 2; } var mulBy2Arrow = a => a * 2;
When To Use It
There’s quite a bit more to arrow functions than are detailed here, including the syntax for zero or more than one parameter, the single-line syntax for returning objects, how Function.prototype.bind
and Function.prototype.apply
are different when used with arrow functions vs. regular functions, using rest parameter syntax rather than the implicit arguments
parameter, and that arrow functions are required to be anonymous. However, in my opinion, none of these details matter for determining when to use arrow functions.
Arrow functions are designed to significantly reduce the use of this
hacks, hopefully making code shorter, more readable, and less error-prone. We’ve already seen an example of erroneous code that’s commonly written by accident or due to poor understanding of this
.
The terseness can also be an asset, especially true when dealing with a lot of one-line callbacks. Again, the readability of the terse syntax is debatable but IMO it’s generally more readable in situations like the one below:
var a = functionThatReturnsLongArray(); // this gets shortened: var reducedArray = a.filter(function(item) { return item.property > THRESHOLD; }).reduce(function(acc, cur) { return acc += cur.property; }, 0); // to this: var reducedArrayArrow = a .filter(item => item.property > THRESHOLD) .reduce(((acc, cur) => acc += cur.property), 0);
Therefore, my recommendation is to use arrow functions in callbacks.
I’m also specifically going to specifically discourage using arrow functions for prototype method declarations:
function Astronauts(root, astronautObjs, nationality) { this.root = root; this.astronautObjs = astronautObjs; this.nationality = nationality; } // do this: Astronauts.prototype.show = function() { ... } // NOT THIS: Astronauts.prototype.DoNotDoThis = () => { ... }
The reason is that typically, when calling a function on an object’s prototype, you want the value of this
to refer to the object that called the function so that it can use the properties associated with the calling object. If you use an arrow function in this case, the function will have no useful value for this
and it will be more error prone to use the values associated with the calling object. If you want a function that doesn’t have a useful value for this
, you’ll write less code, and the code will be less error-prone, by simply writing a function in the global scope.
Please let me know your thoughts on Arrow Functions by leaving a comment below!