Advantages and Pitfalls of Arrow Functions
There are big differences between arrow functions and classic function expressions. They’re more powerful in some situations, but terrible in others.
JavaScript arrow functions were introduced in ES2015. They are a more succinct way of writing functions. For example, foo
and bar
do the same thing:
let foo = function() {
console.log('BAZ')
}let bar = () => {
console.log('BAZ')
}foo() // BAZ
bar() // BAZ
But arrow functions are not just syntactic sugar for the classic function expressions. There are some big differences of which you should be aware. These differences make arrow functions extra awesome, but they can trip you up if you’re not careful. Sometimes arrow functions won’t cut it and you’ll need to use the good old function expressions.
Arrow functions and this
In classic function expressions, the this
keyword is bound to different values based on the context in which the function is called. Whereas arrow functions use the value of this
in their lexical scope. This leads to very different behaviour.
What’s the difference between context and scope? The context is (roughly) the object that calls the function. And the scope is all the variables visible to a function where it is defined. One cares about how it is called, the other cares about how it is defined.
For an example of context, consider an object which which has a method defined by a function expression:
let obj = {
myVar: 'foo',
myFunc: function() {
console.log(this.myVar)
}
}obj.myFunc() // foo
obj
is the object calling myFunc
. It’s myFunc
's context. So the value of this
in myFunc
is bound to obj
. Context can be defined in different ways depending on how a function is called. For example when a constructor is called with the new
keyword, it refers to the the object being created. You can read more about context on MDN.
So if this
is bound to the context (i.e. bound to the object that calls a function), it can lead to some very awkward issues with callbacks. Let’s add a setTimeout
to our obj.myFunc
to simulate a callback:
let obj = {
myVar: 'foo',
myFunc: function() {
console.log(this.myVar)
setTimeout(function() {
console.log(this.myVar)
}, 1000)
}
}obj.myFunc() // foo ... then... undefined
myFunc
's value of this
refers to obj
. so logging myFunc.myVar
from within that function correctly prints 'foo'
. However, the second function is called by setTimeout
— so its context is different. Its context is actually a Timeout
object in Node or the window
object in browsers. So although we probably meant for this
still to refer to obj
, we’ve lost our reference to it.
This requires some gymnastics to get around. One strategy is to assign this
to a variable which is usually named self
or that
. This variable is in the lexical scope of the callback function. This means the callback function can access that variable because it was defined in its scope:
let obj = {
myVar: 'foo',
myFunc: function() {
let self = this console.log(this.myVar)
setTimeout(function() {
console.log(self.myVar)
}, 1000)
}
}obj.myFunc() // foo ... then... foo
You can also achieve this using methods such as bind
, call
, and apply
. These are all different ways of passing in a value to be bound to the this
keyword of a function.
There’s an even cleaner solution to this problem using arrow functions. Recall we said that arrow functions take their value of this from the lexical scope. That means it just uses the value of this in the surrounding code block. It doesn’t care what calls it, it just cares where it was defined. Take a look:
let obj = {
myVar: 'foo',
myFunc: function() {
console.log(this.myVar)
setTimeout(() => {
console.log(this.myVar)
}, 1000)
}
}obj.myFunc() // foo ... then... foo
So immediately we can see that arrow functions are better suited for callbacks. But what happens if we try to use an arrow function as an object method?
let obj = {
myVar: 'foo',
myFunc: () => {
console.log(this.myVar)
}
}obj.myFunc() // undefined
You might expect this
to refer to obj
. But arrow functions don’t bind this
to the object that called them. They just use the value of this
in the scope in which they were defined. In this case, that’s the global object. So arrow functions are unusable for object methods!
The takeaway: Function expressions are best for object methods. Arrow functions are best for callbacks or methods like map, reduce, or forEach.
You can read more about scopes on MDN. On a fundamental level, arrow functions are simply incapable of binding a value of this
different from the value of this
in their scope. So the methods bind
, call
, and apply
will have no effect on them.
Constructors
There’s another way arrow functions don’t work well with objects. They can’t be constructors. The classic function expressions can be used to construct a new object like so:
let Person = function(name, height) {
this.name = name
this.height = height
}Person.prototype.hello = function() {
console.log('Hi, my name is ' + this.name)
}let alice = new Person('Alice', 1.7)
alice.hello() // Hi, my name is Alice
But arrow functions do not have a prototype
property and they cannot be used with new
.
Binding arguments
We’ve seen how arrow functions don’t bind a this
and they just use the value of this
in their scope. Arrow functions also don’t bind an arguments
object. With function expressions, you can do this:
let sum = function() {
let args = Array.from(arguments) // arguments is available return args.reduce((a, b) => a + b, 0)
}sum(1, 2, 3) // 6
Arrow functions don’t have an arguments
object. But the same functionality can be achieved using rest parameters:
let sum4 = (...args) => {
return args.reduce((a, b) => a + b, 0)
}sum(1, 2, 3) // 6
Implicitly returning values
We can make the function above even more concise with arrow functions. Using the concise form, we don’t have to wrap our code in a block. We can just define an expression and the arrow function will automatically return its value:
let sum = (...args) => args.reduce((a, b) => a + b, 0)
On top of this, we can return object literals by wrapping them in parentheses:
let getObj = () => ({ foo: 'hello', bar: 'world' })
This concise syntax makes arrow functions even better for defining small and easily readable callbacks.
Summary
So arrow functions are quite different from the old function expressions. They have some nice properties which allow them to work well as callback functions… but they suck as object methods and can’t be used as constructors. There are a few other differences. For example, arrow functions can’t be generators. I encourage you to check out the details on MDN. Happy hacking!